Исходный код GenServer поведение
Модуль поведения для реализации сервера в клиент-серверном взаимодействии.
GenServer — это процесс, подобный любому другому Elixir-процессу, и он может использоваться для хранения состояния, выполнения кода асинхронно и т. д. Преимущество использования процесса-сервера общего назначения (GenServer), реализованного с помощью этого модуля, заключается в том, что он будет иметь стандартный набор функций интерфейса и включать функциональность для отслеживания и обработки ошибок. Он также будет подходить для размещения в дереве надзора.
graph BT
C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
A & B & C -->|request| GenServer
GenServer -.->|reply| A & B & C
Пример
Поведение GenServer абстрагирует общее взаимодействие клиент-сервер. Разработчики должны реализовать только те обратные вызовы и функциональность, которые их интересуют.
Начнем с примера кода, а затем рассмотрим доступные обратные вызовы. Представьте, что мы хотим реализовать службу с GenServer, которая работает как стек, позволяя нам добавлять и удалять элементы. Мы настроим GenServer общего назначения с помощью собственного модуля, реализовав три обратных вызова.
init/1 преобразует наш начальный аргумент в начальное состояние для GenServer. handle_call/3 срабатывает, когда сервер получает синхронное pop сообщение, удаляет элемент из стека и возвращает его пользователю. handle_cast/2 сработает, когда сервер получит асинхронное push сообщение, добавит элемент в стек:
defmodule Stack do
use GenServer
# Callbacks
@impl true
def init(elements) do
initial_state = String.split(elements, ",", trim: true)
{:ok, initial_state}
end
@impl true
def handle_call(:pop, _from, state) do
[to_caller | new_state] = state
{:reply, to_caller, new_state}
end
@impl true
def handle_cast({:push, element}, state) do
new_state = [element | state]
{:noreply, new_state}
end
end
Мы оставляем механизм работы процесса, передачу сообщений и цикл обработки сообщений поведению GenServer и сосредоточиваемся только на реализации стека. Теперь мы можем использовать API GenServer для взаимодействия со службой, создав процесс и отправив ему сообщения:
# Start the server
{:ok, pid} = GenServer.start_link(Stack, "hello,world")
# This is the client
GenServer.call(pid, :pop)
#=> "hello"
GenServer.cast(pid, {:push, "elixir"})
#=> :ok
GenServer.call(pid, :pop)
#=> "elixir"
Мы запускаем наш Stack вызвав start_link/2, передавая модуль с реализацией сервера и его начальный аргумент со списком элементов через запятую. Поведение GenServer вызывает обратный вызов init/1 для установления начального состояния GenServer. С этого момента GenServer получает управление, поэтому мы взаимодействуем с ним, отправляя два типа сообщений на клиенте. Сообщения типа call ожидают ответ от сервера (и поэтому являются синхронными), а сообщения типа cast — нет.
Каждый вызов GenServer.call/3 приводит к сообщению, которое должно обрабатываться обратным вызовом handle_call/3 в GenServer. Сообщение cast/2 должно обрабатываться handle_cast/2. GenServer поддерживает 8 обратных вызовов, но требуется только init/1.
use GenServerКогда вы
use GenServer, модульGenServerустановит@behaviour GenServerи определит функциюchild_spec/1, поэтому ваш модуль может использоваться как дочерний элемент в дереве надзора.
API клиент / сервер
Хотя в примере выше мы использовали GenServer.start_link/3 и аналогичные функции для непосредственного запуска и общения с сервером, в большинстве случаев мы не вызываем функции GenServer напрямую. Вместо этого мы оборачиваем вызовы в новые функции, представляющие общедоступный API сервера. Эти тонкие оболочки называются клиентским API.
Вот улучшенная реализация нашего модуля Stack:
defmodule Stack do
use GenServer
# Client
def start_link(default) when is_binary(default) do
GenServer.start_link(__MODULE__, default)
end
def push(pid, element) do
GenServer.cast(pid, {:push, element})
end
def pop(pid) do
GenServer.call(pid, :pop)
end
# Server (callbacks)
@impl true
def init(elements) do
initial_state = String.split(elements, ",", trim: true)
{:ok, initial_state}
end
@impl true
def handle_call(:pop, _from, state) do
[to_caller | new_state] = state
{:reply, to_caller, new_state}
end
@impl true
def handle_cast({:push, element}, state) do
new_state = [element | state]
{:noreply, new_state}
end
end
На практике часто бывает, что функции сервера и клиента находятся в одном модуле. Если реализации сервера и/или клиента становятся сложными, вы можете поместить их в разные модули.
На следующей диаграмме обобщаются взаимодействия между клиентом и сервером. Клиент и сервер — это процессы, и общение происходит посредством сообщений (сплошная линия). Взаимодействие Сервер <-> Модуль происходит, когда процесс GenServer вызывает ваш код (пунктирные линии):
sequenceDiagram
participant C as Client (Process)
participant S as Server (Process)
participant M as Module (Code)
note right of C: Typically started by a supervisor
C->>+S: GenServer.start_link(module, arg, options)
S-->>+M: init(arg)
M-->>-S: {:ok, state} | :ignore | {:error, reason}
S->>-C: {:ok, pid} | :ignore | {:error, reason}
note right of C: call is synchronous
C->>+S: GenServer.call(pid, message)
S-->>+M: handle_call(message, from, state)
M-->>-S: {:reply, reply, state} | {:stop, reason, reply, state}
S->>-C: reply
note right of C: cast is asynchronous
C-)S: GenServer.cast(pid, message)
S-->>+M: handle_cast(message, state)
M-->>-S: {:noreply, state} | {:stop, reason, state}
note right of C: send is asynchronous
C-)S: Kernel.send(pid, message)
S-->>+M: handle_info(message, state)
M-->>-S: {:noreply, state} | {:stop, reason, state}
Как настроить надзор
GenServer обычно запускается в дереве надзора. Когда мы вызываем use GenServer, он автоматически определяет функцию child_spec/1, которая позволяет нам запустить Stack непосредственно под надзирателем. Чтобы запустить стандартный стек ["hello", "world"] под надзирателем, мы можем сделать так:
children = [
{Stack, "hello,world"}
]
Supervisor.start_link(children, strategy: :one_for_all)
Обратите внимание, что указание модуля MyServer эквивалентно указанию кортежа {MyServer, []}.
use GenServer также принимает список опций, которые конфигурируют спецификацию дочернего элемента и, следовательно, то, как он работает под надзирателем. Сгенерированную функцию child_spec/1 можно настроить с помощью следующих опций:
-
:id— идентификатор спецификации дочернего элемента, по умолчанию — текущий модуль -
:restart— когда дочерний элемент должен быть перезапущен, по умолчанию —:permanent -
:shutdown— как остановить дочерний элемент, немедленно или дав ему время на остановку
Например:
use GenServer, restart: :transient, shutdown: 10_000
См. раздел «Спецификация дочернего элемента» в модуле Supervisor для получения более подробной информации. Аннотация @doc непосредственно перед use GenServer будет добавлена к сгенерированной функции child_spec/1.
При остановке GenServer, например, путем возвращения кортежа {:stop, reason, new_state} из обратного вызова, код выхода используется надзирателем для определения необходимости перезапуска GenServer. См. раздел «Причины выхода и перезапуска» в модуле Supervisor.
Регистрация имени
И start_link/3, и start/3 поддерживают GenServer для регистрации имени при запуске через опцию :name. Зарегистрированные имена также автоматически очищаются при завершении работы.
атом — GenServer регистрируется локально (на текущем узле) с заданным именем с помощью
Process.register/2.{:global, term}— GenServer регистрируется глобально с заданным термином с помощью функций в модуле:global.{:via, module, term}— GenServer регистрируется с заданным механизмом и именем. Опция:viaожидает модуль, экспортирующий функцииregister_name/2,unregister_name/1,whereis_name/1иsend/2. Одним из таких примеров является модуль:global, который использует эти функции для хранения списка имен процессов и их соответствующих идентификаторов процессов, доступных глобально для сети Elixir-узлов. Elixir также поставляется с локальным, децентрализованным и масштабируемым реестромRegistryдля локального хранения динамически генерируемых имен.
Например, мы могли бы запустить и зарегистрировать наш сервер Stack локально следующим образом:
# Start the server and register it locally with name MyStack
{:ok, _} = GenServer.start_link(Stack, "hello", name: MyStack)
# Now messages can be sent directly to MyStack
GenServer.call(MyStack, :pop)
#=> "hello"
После запуска сервера оставшиеся функции в этом модуле (call/3, cast/2 и др.) также будут принимать атом или любые кортежи {:global, ...} или {:via, ...}. В общем, поддерживаются следующие форматы:
- PID
- атом, если сервер зарегистрирован локально
-
{atom, node}если сервер зарегистрирован локально на другом узле -
{:global, term}если сервер зарегистрирован глобально -
{:via, module, name}если сервер зарегистрирован через альтернативный реестр
Если есть интерес к регистрации динамических имен локально, не используйте атомы, так как атомы никогда не удаляются из памяти, и поэтому динамически сгенерированные атомы не будут удалены. В таких случаях вы можете создать свой собственный локальный реестр с помощью модуля Registry.
Получение «обычных» сообщений
Цель GenServer — абстрагировать цикл «receive» для разработчиков, автоматически обрабатывая системные сообщения, поддерживая изменения кода, синхронные вызовы и многое другое. Поэтому вы никогда не должны вызывать свой собственный «receive» внутри обратных вызовов GenServer, так как это приведет к неправильной работе GenServer.
Помимо синхронной и асинхронной коммуникации, предоставляемой call/3 и cast/2, «обычные» сообщения, отправленные функциями, такими как send/2, Process.send_after/4 и аналогичные, могут обрабатываться в обратном вызове handle_info/2.
handle_info/2 может использоваться во многих ситуациях, таких как обработка сообщений DOWN, отправленных Process.monitor/1. Еще один пример использования handle_info/2 — выполнение периодической работы с помощью Process.send_after/4:
defmodule MyApp.Periodically do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, %{})
end
@impl true
def init(state) do
# Schedule work to be performed on start
schedule_work()
{:ok, state}
end
@impl true
def handle_info(:work, state) do
# Do the desired work here
# ...
# Reschedule once more
schedule_work()
{:noreply, state}
end
defp schedule_work do
# We schedule the work to happen in 2 hours (written in milliseconds).
# Alternatively, one might write :timer.hours(2)
Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
end
end
Таймауты
Возвращаемое значение init/1 или любого из обратных вызовов handle_* может включать значение таймаута в миллисекундах; если нет, предполагается :infinity. Таймаут может использоваться для обнаружения пауз в приходящих сообщениях.
Значение timeout() используется следующим образом:
Если в процессе уже есть ожидающее сообщение, когда возвращается значение
timeout(), таймаут игнорируется, и ожидающее сообщение обрабатывается как обычно. Это означает, что даже таймаут в0миллисекунд не гарантируется для выполнения (если вы хотите выполнить другое действие немедленно и безусловно, используйте инструкцию:continueвместо этого).Если какое-либо сообщение приходит до истечения указанного количества миллисекунд, таймаут сбрасывается, и это сообщение обрабатывается как обычно.
В противном случае, когда истекло указанное количество миллисекунд без поступления сообщения, вызывается
handle_info/2с:timeoutв качестве первого аргумента.
Когда не стоит использовать GenServer
До сих пор мы узнали, что GenServer может использоваться как контролируемый процесс, обрабатывающий синхронные и асинхронные вызовы. Он также может обрабатывать системные сообщения, такие как периодические сообщения и события мониторинга. Процессы GenServer также могут иметь имена.
GenServer, или процесс вообще, должен использоваться для моделирования временных характеристик вашей системы. GenServer никогда не должен использоваться для целей организации кода.
В Elixir организация кода осуществляется с помощью модулей и функций, процессы не нужны. Например, представьте, что вы реализуете калькулятор и решите поместить все операции калькулятора в GenServer:
def add(a, b) do
GenServer.call(__MODULE__, {:add, a, b})
end
def subtract(a, b) do
GenServer.call(__MODULE__, {:subtract, a, b})
end
def handle_call({:add, a, b}, _from, state) do
{:reply, a + b, state}
end
def handle_call({:subtract, a, b}, _from, state) do
{:reply, a - b, state}
end
Это антипаттерн не только потому, что он усложняет логику калькулятора, но и потому, что вы помещаете логику калькулятора за один процесс, который потенциально может стать узким местом в вашей системе, особенно по мере увеличения количества вызовов. Вместо этого просто определите функции непосредственно:
def add(a, b) do a + b end def subtract(a, b) do a - b end
Если вам не нужен процесс, то вам не нужен процесс. Используйте процессы только для моделирования свойств выполнения, таких как изменяемое состояние, конкурентность и сбои, но никогда для организации кода.
Отладка с модулем :sys
GenServer, как специальные процессы, можно отлаживать с помощью модуля :sys. Благодаря различным хукам, этот модуль позволяет разработчикам инспектировать состояние процесса и отслеживать системные события, происходящие во время его выполнения, такие как полученные сообщения, отправленные ответы и изменения состояния.
Давайте рассмотрим основные функции из модуля :sys, используемые для отладки:
-
:sys.get_state/2- позволяет получить состояние процесса. В случае процесса GenServer это будет состояние модуля обратного вызова, переданное в функции обратного вызова в качестве последнего аргумента. -
:sys.get_status/2- позволяет получить статус процесса. Этот статус включает словарь процесса, работает ли процесс или приостановлен, родительский PID, состояние отладчика и состояние модуля поведения, которое включает состояние модуля обратного вызова (как возвращается функцией:sys.get_state/2). Можно изменить представление этого статуса, определив необязательный обратный вызовGenServer.format_status/2. -
:sys.trace/3- выводит все системные события в:stdio. -
:sys.statistics/3- управляет сбором статистики процесса. -
:sys.no_debug/2- отключает все обработчики отладки для данного процесса. Очень важно выключить отладку после завершения работы. Чрезмерное количество обработчиков отладки или тех, которые должны быть отключены, но не были, могут серьезно повредить производительность системы. -
:sys.suspend/2- позволяет приостановить процесс, чтобы он отвечал только на системные сообщения, но не на другие сообщения. Приостановленный процесс можно возобновить с помощью:sys.resume/2.
Давайте посмотрим, как мы можем использовать эти функции для отладки сервера стека, который мы определили ранее.
iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
*DBG* <0.122.0> got cast {push,1}
*DBG* <0.122.0> new state [1]
:ok
iex> :sys.get_state(pid)
[1]
iex> Stack.pop(pid)
*DBG* <0.122.0> got call pop from <0.80.0>
*DBG* <0.122.0> sent 1 to <0.80.0>, new state []
1
iex> :sys.statistics(pid, :get)
{:ok,
[
start_time: {{2016, 7, 16}, {12, 29, 41}},
current_time: {{2016, 7, 16}, {12, 29, 50}},
reductions: 117,
messages_in: 2,
messages_out: 0
]}
iex> :sys.no_debug(pid) # turn off all debug handlers
:ok
iex> :sys.get_status(pid)
{:status, #PID<0.122.0>, {:module, :gen_server},
[
[
"$initial_call": {Stack, :init, 1}, # process dictionary
"$ancestors": [#PID<0.80.0>, #PID<0.51.0>]
],
:running, # :running | :suspended
#PID<0.80.0>, # parent
[], # debugger state
[
header: 'Status for generic server <0.122.0>', # module status
data: [
{'Status', :running},
{'Parent', #PID<0.80.0>},
{'Logged events', []}
],
data: [{'State', [1]}]
]
]}
Дополнительная информация
Если вы хотите узнать больше о GenServers, руководство Elixir Getting Started предоставляет учебное введение. Документация и ссылки в Erlang также могут предоставить дополнительную информацию.
Резюме
Типы
- debug()
Параметры отладки, поддерживаемые функциями
start*- from()
Кортеж, описывающий клиента запроса вызова.
- name()
Имя GenServer
- on_start()
Значения возвращаемых функций
start*- option()
Значения параметров, используемых функциями
start*- options()
Параметры, используемые функциями
start*- server()
Ссылка на сервер.
Обратные вызовы
- code_change(old_vsn, state, extra)
Вызывается для изменения состояния
GenServerпри загрузке другой версии модуля (горячая замена кода), и структура терма состояния должна быть изменена.- format_status(reason, pdict_and_state)
Вызывается в некоторых случаях для получения отформатированного представления статуса
GenServer- handle_call(request, from, state)
Вызывается для обработки синхронных сообщений
call/3.call/3будет блокироваться до получения ответа (если вызов не истечёт или узлы не будут отключены).- handle_cast(request, state)
Вызывается для обработки асинхронных сообщений
cast/2.- handle_continue(continue_arg, state)
Вызывается для обработки инструкций продолжения.
- handle_info(msg, state)
Вызывается для обработки всех остальных сообщений.
- init(init_arg)
Вызывается при запуске сервера.
start_link/3илиstart/3будут блокироваться до его возвращения.- terminate(reason, state)
Вызывается при закрытии сервера. Он должен выполнить все необходимые действия по очистке.
Функции
- abcast(nodes \\ [node() | Node.list()], name, request)
Отправляет все локально зарегистрированные серверы как
nameна указанных узлах.- call(server, request, timeout \\ 5000)
Выполняет синхронный вызов сервера
serverи ожидает его ответа.- cast(server, request)
Отправляет запрос серверу
serverбез ожидания ответа.- multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity)
Вызывает все локально зарегистрированные серверы как
nameна указанныхnodes.- reply(client, reply)
Отвечает клиенту.
- start(module, init_arg, options \\ [])
Запускает процесс
GenServerбез связей (вне дерева надзора).- start_link(module, init_arg, options \\ [])
Запускает процесс
GenServer, связанный с текущим процессом.- stop(server, reason \\ :normal, timeout \\ :infinity)
Синхронно останавливает сервер с указанным
reason.- whereis(server)
Возвращает
pidили{name, node}процесса GenServer,nilв противном случае.
Типы
debug()Source
@type debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t()}] Параметры отладки, поддерживаемые функциями start*
from()Source
@type from() :: {pid(), tag :: term()} Кортеж, описывающий клиента запроса вызова.
pid — PID вызывающей стороны, а tag — уникальное значение, используемое для идентификации вызова.
name()Source
@type name() :: atom() | {:global, term()} | {:via, module(), term()} Имя GenServer
on_start()Source
@type on_start() ::
{:ok, pid()} | :ignore | {:error, {:already_started, pid()} | term()} Возвращаемые значения функций start*
option()Source
@type option() ::
{:debug, debug()}
| {:name, name()}
| {:timeout, timeout()}
| {:spawn_opt, [Process.spawn_opt()]}
| {:hibernate_after, timeout()} Значения параметров, используемые функциями start*
options()Source
@type options() :: [option()]
Параметры, используемые функциями start*
server()Source
@type server() :: pid() | name() | {atom(), node()} Ссылка на сервер.
Это либо обычный PID, либо значение, представляющее зарегистрированное имя. Подробнее об этом см. раздел "Регистрация имён" в этом документе.
Обработчики
code_change(old_vsn, state, extra)Source
@callback code_change(old_vsn, state :: term(), extra :: term()) ::
{:ok, new_state :: term()} | {:error, reason :: term()}
when old_vsn: term() | {:down, term()} Вызывается для изменения состояния GenServer при загрузке другой версии модуля (горячая замена кода), когда структура данных состояния должна измениться.
old_vsn — предыдущая версия модуля (определяется атрибутом @vsn) при обновлении. При переходе к предыдущей версии предыдущая версия обернута в 2-кортеж с первым элементом :down. state — текущее состояние GenServer, а extra — дополнительные данные, необходимые для изменения состояния.
Возвращение {:ok, new_state} изменяет состояние на new_state и успешно выполняется изменение кода.
Возвращение {:error, reason} приводит к ошибке изменения кода с причиной reason и состояние остается предыдущим.
Если code_change/3 вызывает исключение, изменение кода не выполняется, и цикл продолжит выполнение с предыдущим состоянием. Поэтому этот обработчик обычно не содержит побочных эффектов.
Этот обработчик является необязательным.
format_status(reason, pdict_and_state)Source
@callback format_status(reason, pdict_and_state :: list()) :: term() when reason: :normal | :terminate
Вызывается в некоторых случаях для получения отформатированного состояния GenServer:
если вызывается одна из функций
:sys.get_status/1или:sys.get_status/2для получения состоянияGenServer, тоreasonравно:normalесли
GenServerзавершается аварийно и записывает ошибку в журнал, тоreasonравно:terminate
Этот обработчик может быть полезен для управления отображением состояния GenServer. Например, он может использоваться для возвращения компактного представления состояния GenServer для избежания вывода больших терминов состояния.
pdict_and_state — список из двух элементов [pdict, state], где pdict — список кортежей {key, value}, представляющих текущий словарь процесса GenServer, а state — текущее состояние GenServer.
handle_call(request, from, state)Source
@callback handle_call(request :: term(), from(), state :: term()) ::
{:reply, reply, new_state}
| {:reply, reply, new_state,
timeout() | :hibernate | {:continue, continue_arg :: term()}}
| {:noreply, new_state}
| {:noreply, new_state,
timeout() | :hibernate | {:continue, continue_arg :: term()}}
| {:stop, reason, reply, new_state}
| {:stop, reason, new_state}
when reply: term(), new_state: term(), reason: term() Вызывается для обработки синхронных сообщений call/3. call/3 будет блокироваться до получения ответа (если вызов не истечет или узлы не будут отключены).
request — сообщение запроса, отправленное call/3, from — 2-кортеж, содержащий PID вызывающего процесса и терм, уникально идентифицирующий вызов, а state — текущее состояние GenServer.
Возвращение {:reply, reply, new_state} отправляет ответ reply вызывающему процессу и продолжает цикл с новым состоянием new_state.
Возвращение {:reply, reply, new_state, timeout} аналогично {:reply, reply, new_state}, но также устанавливает таймаут. См. раздел «Таймауты» в документации модуля для получения дополнительной информации.
Возвращение {:reply, reply, new_state, :hibernate} аналогично {:reply, reply, new_state}, но процесс переходит в состояние ожидания и возобновляет цикл, когда сообщение есть в очереди сообщений. Однако, если сообщение уже находится в очереди сообщений, процесс сразу возобновит цикл. Перевод GenServer в состояние ожидания вызывает сборку мусора и оставляет непрерывную кучу, что минимизирует используемую память процессом.
Не следует злоупотреблять состоянием ожидания, так как слишком много времени может быть потрачено на сборку мусора, что замедлит обработку входящих сообщений. Обычно его следует использовать только тогда, когда нет ожиданий новых сообщений и минимизация памяти процесса оказывается полезной.
Возвращение {:reply, reply, new_state, {:continue, continue_arg}} аналогично {:reply, reply, new_state}, за исключением того, что handle_continue/2 будет вызван непосредственно после с continue_arg в качестве первого аргумента и state в качестве второго.
Возвращение {:noreply, new_state} не отправляет ответ вызывающему процессу и продолжает цикл с новым состоянием new_state. Ответ необходимо отправить с помощью reply/2.
Существует три основных случая использования, когда не используется возвращаемое значение для ответа:
- Отправить ответ до возвращения из обработчика, так как ответ известен до вызова медленной функции.
- Отправить ответ после возвращения из обработчика, так как ответ еще недоступен.
- Отправить ответ из другого процесса, например, из задачи.
При ответе из другого процесса GenServer должен завершиться, если другой процесс завершится без ответа, так как вызывающий процесс будет блокироваться, ожидая ответа.
Возвращение {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg}} аналогично {:noreply, new_state}, за исключением таймаута, перевода в состояние ожидания или продолжения, как в кортеже :reply.
Возвращение {:stop, reason, reply, new_state} останавливает цикл и вызывает terminate/2 с причиной reason и состоянием new_state. Затем reply отправляется в качестве ответа на вызов, и процесс завершается с причиной reason.
Возвращение {:stop, reason, new_state} аналогично {:stop, reason, reply, new_state}, но ответ не отправляется.
Этот обработчик является необязательным. Если он не реализован, сервер завершится при попытке вызова.
handle_cast(request, state)Source
@callback handle_cast(request :: term(), state :: term()) ::
{:noreply, new_state}
| {:noreply, new_state,
timeout() | :hibernate | {:continue, continue_arg :: term()}}
| {:stop, reason :: term(), new_state}
when new_state: term() Вызывается для обработки асинхронных сообщений cast/2.
request — сообщение запроса, отправленное cast/2, а state — текущее состояние GenServer.
Возвращение {:noreply, new_state} продолжает цикл с новым состоянием new_state.
Возвращение {:noreply, new_state, timeout} аналогично {:noreply, new_state}, но также устанавливает таймаут. См. раздел «Таймауты» в документации модуля для получения дополнительной информации.
Возвращение {:noreply, new_state, :hibernate} аналогично {:noreply, new_state}, но процесс переводится в состояние ожидания перед продолжением цикла. См. handle_call/3 для получения дополнительной информации.
Возвращение {:noreply, new_state, {:continue, continue_arg}} аналогично {:noreply, new_state}, но handle_continue/2 будет вызван сразу после с continue_arg в качестве первого аргумента и state в качестве второго.
Возвращение {:stop, reason, new_state} останавливает цикл и вызывает terminate/2 с причиной reason и состоянием new_state. Процесс завершается с причиной reason.
Этот обработчик является необязательным. Если он не реализован, сервер завершится при попытке вызова.
handle_continue(continue_arg, state)Source
@callback handle_continue(continue_arg, state :: term()) ::
{:noreply, new_state}
| {:noreply, new_state, timeout() | :hibernate | {:continue, continue_arg}}
| {:stop, reason :: term(), new_state}
when new_state: term(), continue_arg: term() Вызывается для обработки инструкций продолжения.
Это полезно для выполнения работы после инициализации или для разделения работы в обработчике на несколько этапов, обновляя состояние процесса по мере выполнения.
Значения возврата такие же, как у handle_cast/2.
Этот обработчик является необязательным. Если он не реализован, сервер завершится при использовании инструкции продолжения.
handle_info(msg, state)Source
@callback handle_info(msg :: :timeout | term(), state :: term()) ::
{:noreply, new_state}
| {:noreply, new_state,
timeout() | :hibernate | {:continue, continue_arg :: term()}}
| {:stop, reason :: term(), new_state}
when new_state: term() Вызывается для обработки всех остальных сообщений.
msg — сообщение, а state — текущее состояние GenServer. При таймауте сообщение равно :timeout.
Значения возврата такие же, как у handle_cast/2.
Этот обработчик является необязательным. Если он не реализован, полученное сообщение будет записано в журнал.
init(init_arg)Source
@callback init(init_arg :: term()) ::
{:ok, state}
| {:ok, state, timeout() | :hibernate | {:continue, continue_arg :: term()}}
| :ignore
| {:stop, reason :: any()}
when state: any() Вызывается при запуске сервера. start_link/3 или start/3 будут ожидать, пока она не вернётся.
init_arg — это аргумент (второй аргумент), переданный в start_link/3.
Возврат значения {:ok, state} заставит start_link/3 вернуть {:ok, pid} и процесс перейдёт к циклу.
Возврат {:ok, state, timeout} аналогичен {:ok, state}, за исключением того, что он также устанавливает таймаут. Более подробную информацию см. в разделе «Таймауты» в документации модуля.
Возврат {:ok, state, :hibernate} аналогичен {:ok, state}, за исключением того, что процесс приостанавливается перед входом в цикл. Более подробную информацию о приостановке см. в handle_call/3.
Возврат {:ok, state, {:continue, continue_arg}} аналогичен {:ok, state}, за исключением того, что сразу после входа в цикл вызывается обратный вызов handle_continue/2 с continue_arg в качестве первого аргумента и state в качестве второго.
Возврат :ignore заставит start_link/3 вернуть :ignore и процесс завершит работу нормально, не войдя в цикл и не вызвав terminate/2. Если используется в дереве управления, родительский супервайзер не потерпит неудачу при запуске, ни немедленно не попытается перезапустить GenServer. Остальная часть дерева управления будет запущена, поэтому обращение к GenServer не требуется другими процессами. Его можно запустить позже с помощью Supervisor.restart_child/2, так как спецификация дочернего процесса сохраняется в родительском супервайзере. Основные варианты использования:
- Сервис
GenServerотключён по конфигурации, но может быть включён позже. - Возникла ошибка, и она будет обработана другим механизмом, отличным от
Supervisor. Скорее всего, этот подход включает вызовSupervisor.restart_child/2через некоторое время, чтобы попытаться перезапустить.
Возврат {:stop, reason} заставит start_link/3 вернуть {:error, reason} и процесс завершится с причиной reason без входа в цикл или вызова terminate/2.
terminate(reason, state)Source
@callback terminate(reason, state :: term()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()} | term() Вызывается при завершении работы сервера. Он должен выполнить необходимые действия по очистке.
reason — это причина выхода, а state — текущее состояние GenServer. Значение возврата игнорируется.
terminate/2 полезен для очистки, требующей доступа к состоянию GenServer. Однако, не гарантируется, что terminate/2 вызывается при завершении GenServer. Поэтому важная очистка должна выполняться с использованием связей процессов и/или мониторинга. Мониторирующий процесс получит ту же причину выхода reason, что и terminate/2.
terminate/2 вызывается, если:
сервер
GenServerловит выходы (используяProcess.flag/2) и родительский процесс (тот, который вызвалstart_link/1) отправляет сигнал выхода-
обратный вызов (кроме
init/1) делает одно из следующего:
Если сервер GenServer является частью дерева управления, он получит сигнал выхода от родительского процесса (своего супервайзера) при завершении работы дерева. Сигнал выхода основан на стратегии завершения в спецификации дочернего процесса, где это значение может быть:
:brutal_kill: серверGenServerуничтожается, поэтомуterminate/2не вызывается.значение таймаута, при котором супервайзер отправит сигнал выхода
:shutdown, а у сервераGenServerбудет время таймаута для завершения. Если после истечения этого таймаута процесс по-прежнему жив, он будет немедленно убит.
Более подробное объяснение см. в разделе «Значения завершения (:shutdown)» в модуле Supervisor.
Если сервер GenServer получит сигнал выхода (который не :normal) от любого процесса, когда он не ловит выходы, он выйдет внезапно с той же причиной, и поэтому не вызовет terminate/2. Обратите внимание, что процесс НЕ ловит выходы по умолчанию, и сигнал выхода отправляется при завершении связанного процесса или отключении узла.
terminate/2 вызывается только после того, как GenServer обработает все сообщения, которые поступили в его почтовый ящик до сигнала выхода. Если он получит сигнал :kill перед завершением обработки этих сообщений, terminate/2 не будет вызван. Если terminate/2 вызывается, любые сообщения, полученные после сигнала выхода, по-прежнему будут в почтовом ящике.
Нет необходимости в очистке, когда GenServer управляет port (например, :gen_tcp.socket) или File.io_device/0, поскольку они будут закрыты при получении сигнала выхода GenServer и не нуждаются в закрытии вручную в terminate/2.
Если reason не является ни :normal, ни :shutdown, ни {:shutdown, term}, регистрируется ошибка.
Этот обратный вызов является необязательным.
Функции
abcast(nodes \\ [node() | Node.list()], name, request)Source
@spec abcast([node()], name :: atom(), term()) :: :abcast
Отправляет сообщение всем серверам, локально зарегистрированным как name, на указанных узлах.
Эта функция возвращает результат немедленно и игнорирует узлы, которые не существуют, или где имя сервера не существует.
См. multi_call/4 для получения дополнительной информации.
call(server, request, timeout \\ 5000)Source
@spec call(server(), term(), timeout()) :: term()
Выполняет синхронный вызов server и ожидает его ответа.
Клиент отправляет заданный request серверу и ожидает, пока не придёт ответ или не истечёт время ожидания. handle_call/3 вызывается на сервере для обработки запроса.
server может принимать любые значения, описанные в разделе «Регистрация имён» документации данного модуля.
Таймауты
timeout — целое число больше нуля, которое определяет, сколько миллисекунд ждать ответа, или атом :infinity для ожидания бесконечно. Значение по умолчанию — 5000. Если ответ не получен в течение указанного времени, вызов функции завершается ошибкой, и вызывающая сторона завершает выполнение. Если вызывающая сторона перехватывает ошибку и продолжает выполнение, а сервер просто опоздал с ответом, он может поступить в очередь сообщений вызывающей стороны в любой момент позднее. В этом случае вызывающая сторона должна быть готова к этому и отбрасывать подобные мусорные сообщения, которые представляют собой кортежи из двух элементов с ссылкой в качестве первого элемента.
cast(server, request)Source
@spec cast(server(), term()) :: :ok
Отправляет запрос server без ожидания ответа.
Эта функция всегда возвращает :ok независимо от того, существует ли целевой server (или узел). Поэтому неизвестно, успешно ли целевой server обработает запрос.
server может принимать любые значения, описанные в разделе «Регистрация имён» документации данного модуля.
multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity)Source
@spec multi_call([node()], name :: atom(), term(), timeout()) ::
{replies :: [{node(), term()}], bad_nodes :: [node()]} Выполняет вызов всех серверов, локально зарегистрированных как name, на указанных nodes.
Сначала request отправляется каждому узлу в nodes; затем вызывающая сторона ожидает ответы. Эта функция возвращает кортеж из двух элементов {replies, bad_nodes}, где:
-
replies- список кортежей{node, reply}, гдеnode- узел, который ответил, аreply- его ответ -
bad_nodes- список узлов, которые либо не существовали, либо на которых не существовал сервер с заданным именемnameили который не ответил
nodes — список имён узлов, которым отправляется запрос. Значение по умолчанию — список всех известных узлов (включая этот узел).
Примеры
Предположим, что GenServer, упомянутый в документации для модуля GenServer, зарегистрирован как Stack на узлах :"foo@my-machine" и :"bar@my-machine".
GenServer.multi_call(Stack, :pop)
#=> {[{:"foo@my-machine", :hello}, {:"bar@my-machine", :world}], []} reply(client, reply)Source
@spec reply(from(), term()) :: :ok
Отправляет ответ клиенту.
Эта функция может использоваться для явного отправления ответа клиенту, который вызвал call/3 или multi_call/4, когда ответ не может быть указан в возвращаемом значении handle_call/3.
client должен быть аргументом from, принятым обратными вызовами handle_call/3. reply — произвольное значение, которое будет возвращено клиенту в качестве результата вызова.
Обратите внимание, что reply/2 может быть вызван из любого процесса, а не только из GenServer, который первоначально получил вызов (если этот GenServer каким-то образом передал аргумент from).
Эта функция всегда возвращает :ok.
Примеры
def handle_call(:reply_in_one_second, from, state) do
Process.send_after(self(), {:reply, from}, 1_000)
{:noreply, state}
end
def handle_info({:reply, from}, state) do
GenServer.reply(from, :one_second_has_passed)
{:noreply, state}
end start(module, init_arg, options \\ [])Source
@spec start(module(), any(), options()) :: on_start()
Запускает процесс GenServer без связей (вне дерева управления).
См. start_link/3 для получения дополнительной информации.
start_link(module, init_arg, options \\ [])Source
@spec start_link(module(), any(), options()) :: on_start()
Запускает процесс GenServer, связанный с текущим процессом.
Это часто используется для запуска GenServer как части дерева управления.
После запуска сервера вызывается функция init/1 заданного модуля с init_arg в качестве аргумента для инициализации сервера. Для обеспечения синхронизированной процедуры запуска эта функция не возвращает значение до тех пор, пока init/1 не вернёт результат.
Обратите внимание, что GenServer, запущенный с помощью start_link/3, связан с родительским процессом и завершит работу в случае сбоя родительского процесса. GenServer также завершит работу по другим причинам, если он настроен на перехват завершений в обратном вызове init/1.
Параметры
:name- используется для регистрации имени, как описано в разделе «Регистрация имён» в документации дляGenServer:timeout- если присутствует, серверу разрешается потратить заданное число миллисекунд на инициализацию, иначе он будет завершён, и функция запуска вернёт{:error, :timeout}:debug- если присутствует, вызывается соответствующая функция в модуле:sys:spawn_opt- если присутствует, его значение передаётся как параметры в подлежащий процесс, как вProcess.spawn/4:hibernate_after- если присутствует, процесс GenServer ожидает любое сообщение в течение заданного числа миллисекунд, и если сообщение не получено, процесс автоматически переходит в состояние гибернации (путем вызова:proc_lib.hibernate/3).
Возвращаемые значения
Если сервер успешно создан и инициализирован, функция возвращает {:ok, pid}, где pid — PID сервера. Если процесс с указанным именем сервера уже существует, функция возвращает {:error, {:already_started, pid}} с PID этого процесса.
Если обратный вызов init/1 завершается ошибкой reason, функция возвращает {:error, reason}. В противном случае, если он возвращает {:stop, reason} или :ignore, процесс завершается, а функция возвращает {:error, reason} или :ignore, соответственно.
stop(server, reason \\ :normal, timeout \\ :infinity)Source
@spec stop(server(), reason :: term(), timeout()) :: :ok
Синхронно останавливает сервер с указанной reason.
Обратный вызов terminate/2 указанного server будет вызван перед завершением. Эта функция возвращает :ok если сервер завершается с указанной причиной; если он завершается с другой причиной, вызов завершается ошибкой.
Эта функция сохраняет семантику OTP в отношении отчётов об ошибках. Если причина отличается от :normal, :shutdown или {:shutdown, _}, генерируется отчёт об ошибке.
whereis(server)Source
@spec whereis(server()) :: pid() | {atom(), node()} | nil Возвращает pid или {name, node} процесса GenServer, nil в противном случае.
Точнее, nil возвращается всякий раз, когда pid или {name, node} не могут быть возвращены. Обратите внимание, что нет гарантии, что возвращённый pid или {name, node} жив, так как процесс может завершиться сразу после того, как он будет найден.
Примеры
Например, чтобы найти процесс сервера, отслеживать его и отправлять ему сообщение:
process = GenServer.whereis(server) monitor = Process.monitor(process) GenServer.cast(process, :hello)
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/GenServer.html