Á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:

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
💡 Supervisor vs DynamicSupervisor

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
Ejercicio 6.1 Diseñar árbol para exchange Intermedio

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.

Ejercicio 6.2 Implementar árbol configurable Avanzado

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
Ejercicio 6.3 Chaos testing Avanzado

Usando el árbol del ejercicio anterior:

  1. Escribe un módulo ChaosTester que mate procesos aleatorios
  2. Verifica que el sistema se recupera correctamente
  3. Mide el tiempo de recuperación
  4. 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)
    └── ...