Aplicaciones OTP

Estructura y empaqueta tu código como una aplicación OTP lista para producción. ⏱️ 2.5 horas

¿Qué es una aplicación OTP?

En OTP, una "aplicación" no es lo que normalmente pensarías. No es un programa ejecutable, sino un componente que agrupa código relacionado, define sus dependencias, y especifica cómo iniciarse y detenerse.

Piensa en una aplicación OTP como un módulo desplegable con:

📊 Aplicaciones en sistemas financieros

Un sistema de trading típico podría tener estas aplicaciones OTP separadas: market_data, order_management, risk_engine, execution. Cada una con su propio árbol de supervisión y ciclo de vida.

Anatomía de una aplicación

Cuando creas un proyecto con mix new mi_app --sup, Mix genera la estructura básica:

mi_app/
├── lib/
│   ├── mi_app.ex              # Módulo principal
│   └── mi_app/
│       └── application.ex     # Callback de aplicación
├── mix.exs                    # Definición del proyecto
└── config/
    └── config.exs             # Configuración

El archivo mix.exs

defmodule MiApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :mi_app,
      version: "1.0.0",
      elixir: "~> 1.15",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Configuración de la aplicación OTP
  def application do
    [
      mod: {MiApp.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  defp deps do
    [
      {:jason, "~> 1.4"},
      {:telemetry, "~> 1.2"}
    ]
  end
end

El módulo Application

defmodule MiApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Lista de hijos del supervisor raíz
      {Registry, keys: :unique, name: MiApp.Registry},
      MiApp.PriceCache,
      {MiApp.FeedSupervisor, strategy: :one_for_one}
    ]

    opts = [strategy: :one_for_one, name: MiApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @impl true
  def stop(_state) do
    IO.puts("Aplicación detenida limpiamente")
    :ok
  end
end

Ciclo de vida de una aplicación

Callback Cuándo se llama Uso típico
start/2 Al iniciar la aplicación Arrancar supervisor raíz
stop/1 Al detener la aplicación Cleanup, cerrar conexiones
prep_stop/1 Antes de stop Preparar shutdown graceful
config_change/3 Hot code reload Reconfiguración en caliente

Tipos de inicio

El primer argumento de start/2 indica cómo se inició:

def start(type, args) do
  case type do
    :normal ->
      # Inicio normal
      IO.puts("Iniciando normalmente")

    {:takeover, node} ->
      # Tomando control de otro nodo (distributed)
      IO.puts("Takeover desde #{node}")

    {:failover, node} ->
      # Failover desde nodo caído
      IO.puts("Failover desde #{node}")
  end

  # ... iniciar supervisor
end

Configuración de aplicaciones

config/config.exs

import Config

# Configuración para todas las aplicaciones
config :mi_app,
  feed_url: "tcp://feed.example.com:9000",
  reconnect_interval: 5_000,
  max_subscribers: 1000

config :mi_app, MiApp.PriceCache,
  ttl: 60_000,
  max_entries: 100_000

# Importar config específica del ambiente
import_config "#{config_env()}.exs"

config/runtime.exs

Para configuración que necesita variables de entorno en runtime:

import Config

if config_env() == :prod do
  config :mi_app,
    feed_url: System.get_env("FEED_URL") ||
      raise "FEED_URL no configurado",
    api_key: System.get_env("API_KEY") ||
      raise "API_KEY no configurado"
end

Acceder a la configuración

# Leer configuración
url = Application.get_env(:mi_app, :feed_url)

# Con valor por defecto
interval = Application.get_env(:mi_app, :reconnect_interval, 5000)

# Leer toda la configuración de la app
all_config = Application.get_all_env(:mi_app)

# Leer config anidada
ttl = Application.get_env(:mi_app, MiApp.PriceCache)[:ttl]

# Fetch! (raise si no existe)
url = Application.fetch_env!(:mi_app, :feed_url)
⚠️ No uses Application.get_env en tiempo de compilación

Evita llamar Application.get_env/3 fuera de funciones (a nivel de módulo). El valor se "congela" en compile-time y no reflejará cambios en runtime. Usa siempre dentro de funciones o en init/1 de GenServers.

Dependencias entre aplicaciones

OTP garantiza que las dependencias arranquen antes que tu aplicación:

def application do
  [
    mod: {MiApp.Application, []},
    # Aplicaciones que DEBEN arrancar antes
    extra_applications: [:logger, :ssl, :crypto]
  ]
end

Las dependencias en deps/0 se añaden automáticamente. Solo necesitas extra_applications para aplicaciones de OTP/Erlang que no están en deps.

Comandos útiles

# Ver aplicaciones cargadas
iex> Application.loaded_applications()
[{:mi_app, 'Mi App', '1.0.0'}, {:logger, ...}, ...]

# Ver aplicaciones iniciadas
iex> Application.started_applications()

# Iniciar/detener manualmente
iex> Application.stop(:mi_app)
iex> Application.start(:mi_app)

# Asegurar que una app está iniciada
iex> Application.ensure_all_started(:mi_app)

Ejemplo: Aplicación de Market Data

defmodule MarketData.Application do
  use Application
  require Logger

  @impl true
  def start(_type, _args) do
    Logger.info("Iniciando MarketData application")

    # Configuración desde Application env
    config = Application.get_all_env(:market_data)

    children = [
      # Registry para nombrar workers por símbolo
      {Registry, keys: :unique, name: MarketData.Registry},

      # Cache de precios global
      {MarketData.PriceCache, config[:cache] || []},

      # Supervisor dinámico para feeds
      {DynamicSupervisor,
        name: MarketData.FeedSupervisor,
        strategy: :one_for_one},

      # Pool de conexiones a exchange
      {MarketData.ConnectionPool, config[:pool] || []},

      # Métricas Telemetry
      MarketData.Telemetry
    ]

    opts = [
      strategy: :one_for_one,
      name: MarketData.Supervisor,
      max_restarts: 10,
      max_seconds: 60
    ]

    Supervisor.start_link(children, opts)
  end

  @impl true
  def prep_stop(state) do
    Logger.info("Preparando shutdown de MarketData")

    # Notificar a subscribers que vamos a cerrar
    MarketData.PriceCache.broadcast(:shutdown_imminent)

    # Dar tiempo a que procesen el mensaje
    Process.sleep(1000)

    state
  end

  @impl true
  def stop(_state) do
    Logger.info("MarketData application detenida")
    :ok
  end
end

Estructura de proyecto recomendada

market_data/
├── lib/
│   ├── market_data.ex                 # API pública
│   └── market_data/
│       ├── application.ex             # Módulo Application
│       ├── price_cache.ex             # GenServer cache
│       ├── feed_supervisor.ex         # DynamicSupervisor
│       ├── feed_worker.ex             # Workers de feeds
│       ├── connection_pool.ex         # Pool de conexiones
│       └── telemetry.ex               # Métricas
├── config/
│   ├── config.exs                     # Config base
│   ├── dev.exs                        # Config desarrollo
│   ├── prod.exs                       # Config producción
│   └── runtime.exs                    # Config runtime
├── test/
│   └── ...
└── mix.exs

El módulo API público

defmodule MarketData do
  @moduledoc """
  API pública para el sistema de market data.

  Este módulo expone las funciones que otros sistemas
  deberían usar para interactuar con market data.
  """

  ## Subscripción a datos

  def subscribe(symbol) do
    MarketData.PriceCache.subscribe(symbol)
  end

  def unsubscribe(symbol) do
    MarketData.PriceCache.unsubscribe(symbol)
  end

  ## Consultas

  def get_price(symbol) do
    MarketData.PriceCache.get(symbol)
  end

  def get_all_prices do
    MarketData.PriceCache.get_all()
  end

  ## Administración de feeds

  def start_feed(symbol, opts \\ []) do
    DynamicSupervisor.start_child(
      MarketData.FeedSupervisor,
      {MarketData.FeedWorker, [{:symbol, symbol} | opts]}
    )
  end

  def stop_feed(symbol) do
    case Registry.lookup(MarketData.Registry, {:feed, symbol}) do
      [{pid, _}] -> DynamicSupervisor.terminate_child(MarketData.FeedSupervisor, pid)
      [] -> {:error, :not_found}
    end
  end
end
Ejercicio 7.1 Crear una aplicación OTP Básico

Crea una aplicación OTP llamada ticker_tracker que:

  • Tenga un GenServer que almacene precios de símbolos
  • Use configuración para definir símbolos iniciales a trackear
  • Exponga una API pública limpia en el módulo principal
  • Implemente prep_stop/1 para loguear estadísticas al cerrar
Ejercicio 7.2 Multi-aplicación umbrella Avanzado

Crea un proyecto umbrella con tres aplicaciones:

  • core - estructuras de datos compartidas
  • feed_handler - recibe datos de mercado
  • api - expone datos via HTTP (usa Plug o Bandit)

Asegúrate de que las dependencias entre apps estén correctamente declaradas y que arranquen en el orden correcto.

Conexión con el proyecto final

El sistema de distribución financiera será una aplicación OTP con: