Как реализовать GenServer?
В Elixir GenServer (Generic Server) — это поведенческий модуль (behavior), предоставляющий стандартный способ реализации серверного процесса с внутренним состоянием. Он используется для управления состоянием, обработки сообщений и построения устойчивых компонентов в архитектуре OTP.
Ниже — пошаговое объяснение, как реализовать GenServer.
1. Подключение поведения GenServer
Первым делом необходимо объявить модуль, использующий поведение GenServer:
defmodule MyServer do
use GenServer
end
2. Определение API модуля (публичные функции)
Обычно создаются функции start_link/1, а также вызовы call/2 и cast/2:
def start_link(initial_state) do
GenServer.start_link(\__MODULE_\_, initial_state, name: \__MODULE_\_)
end
def increment() do
GenServer.cast(\__MODULE_\_, :increment)
end
def get() do
GenServer.call(\__MODULE_\_, :get)
end
-
start_link/1 запускает процесс и связывает его с текущим;
-
cast/2 — асинхронный вызов (не ожидает ответа);
-
call/2 — синхронный вызов (ожидает ответ).
3. Реализация обязательных колбэков
Минимальный набор:
init/1
Вызывается при запуске:
def init(initial_state) do
{:ok, initial_state}
end
handle_cast/2
Обработка асинхронных сообщений:
def handle_cast(:increment, state) do
{:noreply, state + 1}
end
handle_call/3
Обработка синхронных запросов:
def handle_call(:get, \_from, state) do
{:reply, state, state}
end
Пример полной реализации
defmodule Counter do
use GenServer
\# Публичный API
def start_link(initial_value \\\\ 0) do
GenServer.start_link(\__MODULE_\_, initial_value, name: \__MODULE_\_)
end
def increment do
GenServer.cast(\__MODULE_\_, :increment)
end
def get do
GenServer.call(\__MODULE_\_, :get)
end
\# Внутренние колбэки
def init(initial_state) do
{:ok, initial_state}
end
def handle_cast(:increment, state) do
{:noreply, state + 1}
end
def handle_call(:get, \_from, state) do
{:reply, state, state}
end
end
4. Использование GenServer
\# Запустить сервер
{:ok, \_pid} = Counter.start_link(10)
\# Увеличить значение
Counter.increment()
Counter.increment()
\# Получить текущее значение
Counter.get()
\# => 12
5. Дополнительные функции и фичи
handle_info/2
Используется для обработки произвольных сообщений (например, от send(self(), :msg)):
def handle_info(:timeout, state) do
IO.puts("Таймер сработал")
{:noreply, state}
end
terminate/2
Вызывается при остановке процесса:
def terminate(\_reason, \_state) do
:ok
end
code_change/3
Позволяет реализовать hot code upgrades (обычно не нужен на старте):
def code_change(\_old_vsn, state, \_extra), do: {:ok, state}
6. Обработка состояний и таймеров
GenServer может возвращать {:noreply, new_state, timeout}:
def handle_cast(:start_timer, state) do
{:noreply, state, 5000} # Через 5 секунд сработает handle_info(:timeout, ...)
end
7. Регистрация имени
start_link/3 можно вызвать с именем:
GenServer.start_link(\__MODULE_\_, initial, name: :my_counter)
GenServer.call(:my_counter, :get)
Это удобно при обращении к серверу по имени вместо PID.
8. Использование в супервизоре
GenServer можно встроить в супервизор:
children = \[
{Counter, 0}
\]
Supervisor.start_link(children, strategy: :one_for_one)
Супервизор сам будет перезапускать GenServer при сбое.
9. Отладка GenServer
-
Использовать IO.inspect(state) в колбэках;
-
Использовать :sys.get_state(pid) для отладки состояния;
-
Использовать :observer.start() для визуализации процесса.
10. Ошибки и перезапуски
Если в процессе GenServer возникает исключение — он завершится с ошибкой. Если он под супервизором, то будет перезапущен согласно стратегии. Возможна реализация устойчивых компонентов через try/catch, rescue, handle_info({:EXIT, _}, state).