Inicio / Elixir / Phoenix Framework: Web en Tiempo Real / GenServer en Phoenix

GenServer en Phoenix

GenServer en apps Phoenix, cache, rate limiting y estado.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

GenServer en Phoenix

Introducción

GenServer es el proceso genérico de OTP para manejar estado y ejecutar lógica en background. En Phoenix, los GenServers se integran al supervision tree para crear workers resilientes y servicios con estado persistente.

Background Workers

Un GenServer básico como worker en background:

defmodule MyApp.Workers.CacheWarmer do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(_opts) do
    schedule_warm()
    {:ok, %{last_run: nil}}
  end

  def handle_info(:warm_cache, state) do
    datos = MyApp.Catalogo.list_productos_populares()
    MyApp.Cache.put("productos_populares", datos)
    schedule_warm()
    {:noreply, %{state | last_run: DateTime.utc_now()}}
  end

  defp schedule_warm do
    Process.send_after(self(), :warm_cache, :timer.minutes(5))
  end
end

State Management

Gestión de estado con llamadas síncronas y asíncronas:

defmodule MyApp.Workers.Contador do
  use GenServer

  # API pública
  def start_link(inicial), do: GenServer.start_link(__MODULE__, inicial, name: __MODULE__)
  def valor, do: GenServer.call(__MODULE__, :valor)
  def incrementar, do: GenServer.cast(__MODULE__, :incrementar)
  def reset, do: GenServer.call(__MODULE__, :reset)

  # Callbacks
  def init(valor_inicial), do: {:ok, valor_inicial}

  def handle_call(:valor, _from, state), do: {:reply, state, state}
  def handle_call(:reset, _from, _state), do: {:reply, :ok, 0}

  def handle_cast(:incrementar, state), do: {:noreply, state + 1}
end

Registry

El Registry permite localizar procesos por nombre dinámico:

defmodule MyApp.SalaRegistry do
  def child_spec(_opts) do
    Registry.child_spec(keys: :unique, name: __MODULE__)
  end

  def via(sala_id) do
    {:via, Registry, {__MODULE__, sala_id}}
  end

  def buscar(sala_id) do
    case Registry.lookup(__MODULE__, sala_id) do
      [{pid, _}] -> {:ok, pid}
      [] -> {:error, :not_found}
    end
  end
end

defmodule MyApp.Sala do
  use GenServer

  def start_link(sala_id) do
    GenServer.start_link(__MODULE__, sala_id, name: MyApp.SalaRegistry.via(sala_id))
  end

  def init(sala_id) do
    {:ok, %{id: sala_id, usuarios: [], mensajes: []}}
  end
end

DynamicSupervisor

Para crear procesos bajo demanda:

defmodule MyApp.SalaSupervisor do
  use DynamicSupervisor

  def start_link(init_arg) do
    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one)
  end

  def crear_sala(sala_id) do
    spec = {MyApp.Sala, sala_id}
    DynamicSupervisor.start_child(__MODULE__, spec)
  end

  def cerrar_sala(sala_id) do
    case MyApp.SalaRegistry.buscar(sala_id) do
      {:ok, pid} -> DynamicSupervisor.terminate_child(__MODULE__, pid)
      error -> error
    end
  end
end

Agent

Agent simplifica el manejo de estado cuando no necesitamos lógica compleja:

defmodule MyApp.ConfigStore do
  use Agent

  def start_link(_opts) do
    Agent.start_link(fn -> cargar_config() end, name: __MODULE__)
  end

  def get(clave) do
    Agent.get(__MODULE__, &Map.get(&1, clave))
  end

  def put(clave, valor) do
    Agent.update(__MODULE__, &Map.put(&1, clave, valor))
  end

  def all do
    Agent.get(__MODULE__, & &1)
  end

  defp cargar_config do
    %{
      max_upload_size: 10_000_000,
      maintenance_mode: false,
      feature_flags: %{nuevo_dashboard: true}
    }
  end
end

Task.Supervisor

Para ejecutar tareas asíncronas supervisadas:

defmodule MyApp.TaskRunner do
  def enviar_notificaciones(usuarios, mensaje) do
    Task.Supervisor.async_stream_nolink(
      MyApp.TaskSupervisor,
      usuarios,
      fn usuario ->
        MyApp.Notificaciones.enviar(usuario, mensaje)
      end,
      max_concurrency: 10,
      timeout: 30_000
    )
    |> Enum.reduce({0, 0}, fn
      {:ok, {:ok, _}}, {ok, err} -> {ok + 1, err}
      _, {ok, err} -> {ok, err + 1}
    end)
  end

  def procesar_en_background(fun) do
    Task.Supervisor.start_child(MyApp.TaskSupervisor, fun)
  end
end

Integración con el Supervision Tree

Todos los procesos se registran en el árbol de supervisión de la aplicación:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      MyAppWeb.Telemetry,
      MyApp.Repo,
      {Registry, keys: :unique, name: MyApp.SalaRegistry},
      {Task.Supervisor, name: MyApp.TaskSupervisor},
      MyApp.SalaSupervisor,
      MyApp.Workers.CacheWarmer,
      MyApp.ConfigStore,
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyAppWeb.Endpoint
    ]

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

El orden importa: los procesos se inician secuencialmente de arriba a abajo.

Resumen

GenServer en Phoenix permite crear workers con estado, procesos en background y servicios resilientes. Registry y DynamicSupervisor gestionan procesos dinámicos, Agent simplifica el estado simple, y Task.Supervisor ejecuta tareas asíncronas. Todo se integra en el supervision tree de OTP para máxima fiabilidad.

🔒

Ejercicio práctico disponible

GenServer para cache en Phoenix

Desbloquear ejercicios
// GenServer para cache en Phoenix
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes