Что такое Elixir Agents и когда их использовать вместо GenServer?
Agent — обёртка над GenServer для хранения состояния без кастомной логики сообщений. Когда нужны кастомные handle_call/handle_cast, таймауты, handle_info или сложный lifecycle — используйте GenServer напрямую.
Agent vs GenServer: в чём разница
Agent — это тонкая абстракция над GenServer, предназначенная исключительно для управления состоянием. Он скрывает шаблонный код: вам не нужно объявлять init/1, handle_call/3 и handle_cast/2 — достаточно передать функцию-трансформер состояния. GenServer даёт полный контроль над протоколом сообщений, lifecycle и обработкой ошибок.
Agent: когда и как
Используйте Agent, когда процесс — это просто хранилище данных с простыми операциями чтения/записи.
defmodule Counter do
def start_link(initial \\ 0) do
Agent.start_link(fn -> initial end, name: __MODULE__)
end
def increment do
Agent.update(__MODULE__, &(&1 + 1))
end
def value do
Agent.get(__MODULE__, & &1)
end
end
# Использование
{:ok, _pid} = Counter.start_link(0)
Counter.increment()
Counter.increment()
IO.inspect(Counter.value()) # => 2
Agent.get/2 — синхронный вызов (call), Agent.update/2 тоже синхронный. Есть Agent.cast/2 для асинхронных обновлений без ожидания ответа. Функция, переданная в Agent, выполняется внутри процесса-агента — длинные вычисления заблокируют всех вызывающих.
GenServer: когда и как
Используйте GenServer, когда нужны:
- Кастомные сообщения через
handle_info/2(например,:timeout, сигналы от других процессов) - Разные типы запросов с разной логикой (
handle_callvshandle_cast) - Инициализация с побочными эффектами (подключение к БД, подписка на события)
- Тонкая обработка ошибок:
{:stop, reason, state} - Совместимость с
:sys.get_state/1для отладки
defmodule RateLimiter do
use GenServer
@window_ms 60_000
@max_requests 100
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def allow?(client_id) do
GenServer.call(__MODULE__, {:check, client_id})
end
## Callbacks
@impl true
def init(_opts) do
# Периодически сбрасываем счётчики
schedule_reset()
{:ok, %{}}
end
@impl true
def handle_call({:check, client_id}, _from, state) do
count = Map.get(state, client_id, 0)
if count < @max_requests do
{:reply, true, Map.put(state, client_id, count + 1)}
else
{:reply, false, state}
end
end
@impl true
def handle_info(:reset, _state) do
schedule_reset()
{:noreply, %{}} # Сбрасываем все счётчики
end
defp schedule_reset do
Process.send_after(self(), :reset, @window_ms)
end
end
Здесь handle_info(:reset, ...) обрабатывает сообщение от таймера — это невозможно реализовать через Agent.
Правило выбора
| Критерий | Agent | GenServer |
|---|---|---|
| Только хранение состояния | ✓ | избыточно |
| Кастомные сообщения / handle_info | ✗ | ✓ |
| Инициализация с побочными эффектами | ✗ | ✓ |
| Разные типы запросов | ✗ | ✓ |
| Простой счётчик / кэш / аккумулятор | ✓ | ✓ |
На практике: если через месяц понадобится добавить handle_info — придётся переписывать Agent в GenServer. Если логика уже нетривиальна, сразу пишите GenServer.
Supervision
Оба модуля одинаково хорошо живут под Supervisor. Agent.child_spec/1 генерируется автоматически, для GenServer — тоже, если определён start_link/1:
children = [
Counter, # Agent
RateLimiter # GenServer
]
Supervisor.start_link(children, strategy: :one_for_one)
Подводные камни
- Долгие вычисления в Agent.get/2 — функция выполняется в процессе агента, блокируя других вызывающих. Для тяжёлых операций используйте
Agent.get_and_update/2или переходите наGenServerс явным копированием состояния на сторону клиента. - Agent не поддерживает handle_info — сообщения от
Process.send_after/3, мониторов или связанных процессов будут проигнорированы или упадут с ошибкой. - Отладка Agent сложнее —
:sys.get_state/1работает, но нет возможности подключить кастомныйformat_status/2для структурированного вывода. - Анонимные агенты теряются — если не передать
name:, ссылку на PID нужно хранить самостоятельно. После рестарта супервизора PID меняется. - Гонка в get + update — последовательность
Agent.get→Agent.updateне атомарна. ИспользуйтеAgent.get_and_update/2для атомарных операций «прочитать и изменить». - Переусложнённый Agent — признак неправильной абстракции — если в Agent накапливается бизнес-логика, это сигнал перейти на GenServer с явными callback-функциями.
- Не путайте с Elixir Task —
Taskдля одноразовых асинхронных вычислений,Agentдля долгоживущего состояния. Нередко новички используютAgentтам, где достаточноTask.async/await. - ETS как альтернатива — для высококонкурентного чтения (тысячи процессов одновременно)
:etsс режимом:read_concurrencyбыстрее, чем любой агент, поскольку исключает bottleneck единственного процесса.
Common mistakes
- Сводить agents vs genserver к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Elixir компилируется в BEAM bytecode и наследует процессы, message passing, supervision и hot-code friendly модель Erlang VM.
- Не отделять validation, authorization, transaction boundary и business logic.
- Менять похожие API местами без учёта семантики ошибок и ownership.
What the interviewer is testing
- Объясняет agents vs genserver через конкретную точку lifecycle в Elixir.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.