Как реализовать 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).