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:
- Un árbol de supervisión raíz
- Configuración de arranque
- Dependencias explícitas
- Metadata (versión, descripción, etc.)
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)
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
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/1para loguear estadísticas al cerrar
Crea un proyecto umbrella con tres aplicaciones:
core- estructuras de datos compartidasfeed_handler- recibe datos de mercadoapi- 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:
- Application module: coordina el arranque ordenado
- Múltiples supervisors: FeedSupervisor, SubscriberSupervisor
- Configuración por ambiente: dev, staging, prod
- Runtime config: secrets y URLs desde variables de entorno
- Graceful shutdown: notificar subscribers antes de cerrar