Árboles de Supervisión
Diseña jerarquías de procesos robustas que aíslan fallos y maximizan la disponibilidad. ⏱️ 2.5 horas
De supervisores planos a árboles
En el capítulo anterior vimos supervisores con hijos directos. En aplicaciones reales, organizamos procesos en árboles donde los supervisores pueden supervisar otros supervisores.
# Árbol plano (lo que vimos)
Supervisor
├── Worker1
├── Worker2
└── Worker3
# Árbol jerárquico (lo que necesitamos)
ApplicationSupervisor
├── CoreSupervisor
│ ├── Registry
│ ├── Cache
│ └── PubSub
├── FeedSupervisor
│ ├── FeedConnection1
│ ├── FeedConnection2
│ └── FeedConnection3
└── ClientSupervisor
├── ClientHandler1
├── ClientHandler2
└── ...
Esta estructura tiene ventajas críticas:
- Aislamiento de fallos: si un feed falla repetidamente, solo FeedSupervisor se ve afectado
- Estrategias por contexto: cada subsistema puede tener su propia estrategia de reinicio
- Orden de inicio: Core se inicia antes que Feeds, que se inician antes que Clients
- Shutdown ordenado: al parar, Clients se cierran antes que Feeds, que cierran antes que Core
Principios de diseño
1. Agrupar por ciclo de vida
Procesos que deben vivir y morir juntos van bajo el mismo supervisor:
# Mal: Parser y Validator separados
MainSupervisor
├── Parser
└── Validator
# Bien: juntos porque Validator depende de Parser
MainSupervisor
└── PipelineSupervisor (:rest_for_one)
├── Parser
└── Validator
2. Aislar lo volátil de lo estable
Conexiones externas (feeds, APIs) fallan más que procesos internos. Sepáralos para que sus fallos no afecten al core:
# Los feeds externos pueden fallar
# El cache interno no debería verse afectado
AppSupervisor
├── StableSupervisor (:one_for_one)
│ ├── Cache # Casi nunca falla
│ ├── Registry # Casi nunca falla
│ └── Metrics # Casi nunca falla
└── VolatileSupervisor (:one_for_one, max_restarts: 20)
├── NasdaqFeed # Puede fallar por red
├── NYSEFeed # Puede fallar por red
└── CryptoFeed # Puede fallar por red
3. Respetar dependencias
Si B depende de A, A debe iniciarse primero. Los hijos se inician en orden de lista:
def init(_) do
children = [
# 1. Primero el Registry (otros lo necesitan para registrarse)
{Registry, keys: :unique, name: MyApp.Registry},
# 2. Luego el Cache (feeds escribirán aquí)
MyApp.Cache,
# 3. Luego los Feeds (necesitan Registry y Cache)
MyApp.FeedSupervisor,
# 4. Finalmente los Clients (necesitan todo lo anterior)
MyApp.ClientSupervisor
]
Supervisor.init(children, strategy: :one_for_one)
end
Ejemplo completo: Sistema de trading
Diseñemos el árbol de supervisión para un sistema de distribución de datos de mercado:
defmodule TradingApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Infraestructura core
TradingApp.Core.Supervisor,
# Ingesta de datos
TradingApp.Feeds.Supervisor,
# Distribución a clientes
TradingApp.Clients.Supervisor
]
opts = [strategy: :one_for_one, name: TradingApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Core Supervisor
defmodule TradingApp.Core.Supervisor do
use Supervisor
def start_link(_) do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(:ok) do
children = [
# Registry para nombrar procesos dinámicamente
{Registry, keys: :unique, name: TradingApp.Registry},
# PubSub para broadcasting interno
{Phoenix.PubSub, name: TradingApp.PubSub},
# Cache de últimos precios (ETS)
TradingApp.PriceCache,
# Métricas y telemetría
TradingApp.Metrics
]
# rest_for_one: si Registry falla, todo lo que depende de él se reinicia
Supervisor.init(children, strategy: :rest_for_one)
end
end
Feeds Supervisor
defmodule TradingApp.Feeds.Supervisor do
use Supervisor
def start_link(_) do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(:ok) do
feeds = Application.get_env(:trading_app, :feeds, [])
children = Enum.map(feeds, fn feed_config ->
# Cada feed tiene su propio supervisor
Supervisor.child_spec(
{TradingApp.Feeds.FeedSupervisor, feed_config},
id: feed_config.name
)
end)
# one_for_one: feeds son independientes entre sí
Supervisor.init(children,
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 60
)
end
end
defmodule TradingApp.Feeds.FeedSupervisor do
use Supervisor
def start_link(config) do
Supervisor.start_link(__MODULE__, config, name: via(config.name))
end
defp via(name), do: {:via, Registry, {TradingApp.Registry, {:feed_sup, name}}}
@impl true
def init(config) do
children = [
# Conexión al feed externo
{TradingApp.Feeds.Connection, config},
# Parser específico para este feed
{TradingApp.Feeds.Parser, config},
# Validador de datos
{TradingApp.Feeds.Validator, config}
]
# rest_for_one: si Connection falla, Parser y Validator también reinician
Supervisor.init(children, strategy: :rest_for_one)
end
end
Clients Supervisor (dinámico)
defmodule TradingApp.Clients.Supervisor do
use DynamicSupervisor
def start_link(_) do
DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(:ok) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 100,
max_seconds: 5
)
end
@doc "Llamado cuando un nuevo cliente se conecta"
def start_client(socket) do
spec = {TradingApp.Clients.Handler, socket}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@doc "Contar clientes conectados"
def count_clients do
DynamicSupervisor.count_children(__MODULE__).active
end
end
Visualizando el árbol
Usa Observer para ver el árbol en tiempo real:
iex> :observer.start()
Ve a la pestaña "Applications" y selecciona tu aplicación para ver el árbol completo con todos los procesos.
También puedes inspeccionarlo programáticamente:
# Ver hijos de un supervisor
Supervisor.which_children(TradingApp.Supervisor)
# Función recursiva para imprimir el árbol
defmodule TreePrinter do
def print(sup, indent \\ 0) do
prefix = String.duplicate(" ", indent)
Supervisor.which_children(sup)
|> Enum.each(fn {id, pid, type, _modules} ->
IO.puts("#{prefix}├── #{id} (#{type}) #{inspect(pid)}")
if type == :supervisor and is_pid(pid) do
print(pid, indent + 1)
end
end)
end
end
TreePrinter.print(TradingApp.Supervisor)
Patrones avanzados
Supervisor con estado inicial
A veces necesitas pasar configuración a los hijos:
defmodule ConfigurableSupervisor do
use Supervisor
def start_link(config) do
Supervisor.start_link(__MODULE__, config, name: __MODULE__)
end
@impl true
def init(config) do
children = [
# Pasar config a cada hijo
{WorkerA, config.worker_a_opts},
{WorkerB, config.worker_b_opts},
# O usar Supervisor.child_spec para personalizar
Supervisor.child_spec(
{WorkerC, config.worker_c_opts},
id: :worker_c_custom_id,
restart: :transient
)
]
Supervisor.init(children, strategy: :one_for_one)
end
end
Múltiples instancias del mismo worker
def init(_) do
# Crear 5 workers del mismo tipo con IDs únicos
children = for i <- 1..5 do
Supervisor.child_spec(
{MyWorker, worker_id: i},
id: {:worker, i}
)
end
Supervisor.init(children, strategy: :one_for_one)
end
Supervisor que añade hijos dinámicamente
defmodule HybridSupervisor do
use Supervisor
def start_link(_) do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(:ok) do
children = [
# Hijos estáticos
StaticWorker
]
Supervisor.init(children, strategy: :one_for_one)
end
@doc "Añadir un hijo dinámicamente"
def add_worker(config) do
spec = Supervisor.child_spec(
{DynamicWorker, config},
id: config.id,
restart: :transient
)
Supervisor.start_child(__MODULE__, spec)
end
def remove_worker(id) do
Supervisor.terminate_child(__MODULE__, id)
Supervisor.delete_child(__MODULE__, id)
end
end
Usa Supervisor cuando conoces los hijos de antemano y
quieres añadir/eliminar ocasionalmente. Usa DynamicSupervisor
cuando los hijos se crean/destruyen constantemente (como conexiones
de clientes).
Shutdown ordenado
Cuando un supervisor se detiene, termina sus hijos en orden inverso
al de inicio. El valor shutdown controla cuánto tiempo
esperar:
# En child_spec
%{
id: MyWorker,
start: {MyWorker, :start_link, []},
shutdown: 5000 # 5 segundos para terminar gracefully
}
# Valores especiales
shutdown: :brutal_kill # Matar inmediatamente
shutdown: :infinity # Esperar indefinidamente (para supervisores)
shutdown: 30_000 # 30 segundos (para workers con cleanup pesado)
En el worker, implementa terminate/2 para cleanup:
defmodule WorkerWithCleanup do
use GenServer
# ... init y handles ...
@impl true
def terminate(reason, state) do
Logger.info("Shutting down: #{inspect(reason)}")
# Cerrar conexiones
if state.socket, do: :gen_tcp.close(state.socket)
# Flush datos pendientes
flush_buffer(state.buffer)
# Notificar a otros procesos
send(state.parent, {:worker_terminated, self()})
:ok
end
end
Diseña (en papel o código) el árbol de supervisión para un exchange simplificado con:
- OrderBook por cada símbolo (BTCUSD, ETHUSD, etc.)
- MatchingEngine que procesa órdenes
- TradeReporter que registra trades ejecutados
- Conexiones de clientes (traders)
- Feed de precios saliente
Define qué estrategia usar en cada nivel y justifica tus decisiones.
Implementa un árbol donde:
- La lista de feeds se lee de configuración
- Cada feed tiene su parser configurado (JSON, FIX, binary)
- Hay un supervisor por feed con Connection + Parser + Validator
- Puedes añadir/eliminar feeds en runtime sin reiniciar la app
Usando el árbol del ejercicio anterior:
- Escribe un módulo
ChaosTesterque mate procesos aleatorios - Verifica que el sistema se recupera correctamente
- Mide el tiempo de recuperación
- Identifica puntos débiles donde el fallo causa más daño
Conexión con el proyecto final
El sistema FinFeed usará esta estructura:
FinFeed.Supervisor
├── Core.Supervisor (:rest_for_one)
│ ├── Registry
│ ├── PG (Process Groups)
│ ├── PriceCache (ETS)
│ └── Telemetry
├── Feeds.Supervisor (:one_for_one)
│ ├── Feed.Supervisor (nasdaq) (:rest_for_one)
│ │ ├── Connection
│ │ ├── Parser
│ │ └── Publisher
│ └── Feed.Supervisor (crypto) (:rest_for_one)
│ ├── Connection
│ ├── Parser
│ └── Publisher
└── Clients.DynamicSupervisor (:one_for_one)
├── ClientHandler (pid1)
├── ClientHandler (pid2)
└── ...