GenServer поведение
Модуль поведения для реализации сервера в клиент-серверной связи.
GenServer — это процесс, подобный любому другому Elixir-процессу, и он может использоваться для хранения состояния, выполнения кода асинхронно и так далее. Преимущество использования универсального серверного процесса (GenServer), реализованного с помощью этого модуля, заключается в том, что у него будет стандартный набор функций интерфейса и функция отслеживания и обработки ошибок. Он также будет подходить для древовидной структуры надзора.
Пример
Поведение GenServer абстрагирует общее взаимодействие клиент-сервер. Разработчики должны только реализовать вызовы и функциональность, которые их интересуют.
Начнем с примера кода, а затем рассмотрим доступные обратные вызовы. Представьте, что нам нужен GenServer, который работает как стек, позволяя нам добавлять и извлекать элементы:
defmodule Stack do
use GenServer
# Callbacks
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])
# This is the client
GenServer.call(pid, :pop)
#=> :hello
GenServer.cast(pid, {:push, :world})
#=> :ok
GenServer.call(pid, :pop)
#=> :world
Мы запускаем наш Stack вызовом start_link/2, передавая модуль с реализацией сервера и его начальный аргумент (список, представляющий стек, содержащий элемент :hello). Мы можем в основном взаимодействовать с сервером, отправляя два типа сообщений. Сообщения типа call ожидают ответ от сервера (и поэтому являются синхронными), в то время как сообщения типа cast ответа не ожидают.
Каждый раз, когда вы выполняете GenServer.call/3, клиент отправляет сообщение, которое должно обрабатываться обратным вызовом handle_call/3 в GenServer. Сообщение типа cast/2 должно обрабатываться handle_cast/2. Существует 7 возможных обратных вызовов, которые необходимо реализовать при использовании GenServer. Единственный обязательный обратный вызов — init/1.
Клиент / Сервер API
Хотя в приведенном выше примере мы использовали GenServer.start_link/3 и другие функции для прямого запуска и взаимодействия с сервером, в большинстве случаев мы не вызываем функции GenServer напрямую. Вместо этого мы оборачиваем вызовы в новые функции, представляющие публичный API сервера.
Вот более совершенная реализация нашего модуля Stack:
defmodule Stack do
use GenServer
# Client
def start_link(default) when is_list(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(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
На практике часто бывает, что функции сервера и клиента находятся в одном модуле. Если реализации сервера и/или клиента усложняются, вы можете захотеть разместить их в разных модулях.
Как организовать надзор
GenServer обычно запускается в дереве надзора. При вызове use GenServer, он автоматически определяет функцию child_spec/1, которая позволяет запустить Stack непосредственно под надзирателем. Чтобы запустить стандартный стек [:hello] под надзирателем, можно сделать следующее:
children = [
{Stack, [:hello]}
]
Supervisor.start_link(children, strategy: :one_for_all)
Обратите внимание, что вы также можете запустить его просто как Stack, что эквивалентно {Stack, []}:
children = [
Stack # The same as {Stack, []}
]
Supervisor.start_link(children, strategy: :one_for_all)
В обоих случаях Stack.start_link/1 всегда вызывается.
use GenServer также принимает список опций, которые конфигурируют спецификацию дочернего элемента и, следовательно, то, как он работает под надзирателем. Сгенерированную функцию child_spec/1 можно настроить с помощью следующих опций:
-
:id— идентификатор спецификации дочернего элемента, по умолчанию равен текущему модулю -
:restart— когда дочерний элемент должен быть перезапущен, по умолчанию:permanent -
:shutdown— как остановить дочерний элемент, сразу или дав ему время на завершение
Например:
use GenServer, restart: :transient, shutdown: 10_000
См. раздел «Спецификация дочернего элемента» в модуле Supervisor для получения более подробной информации. Аннотация @doc непосредственно перед use GenServer будет добавлена к сгенерированной функции child_spec/1.
Регистрация имени
И 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, который использует эти функции для хранения списка имен процессов и их соответствующих PID, доступных глобально для сети 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 — абстрагировать цикл «получения» для разработчиков, автоматически обрабатывая системные сообщения, поддерживая изменения кода, синхронные вызовы и многое другое. Поэтому вы никогда не должны вызывать собственный «прием» внутри обратных вызовов GenServer, так как это приведет к некорректной работе GenServer.
Помимо синхронной и асинхронной связи, предоставляемой call/3 и cast/2, «обычные» сообщения, отправленные функциями, такими как Kernel.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
# In 2 hours
Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
end
end
Таймауты
Значение возвращаемое init/1 или любым из обратных вызовов handle_* может включать значение таймаута в миллисекундах; если нет, предполагается :infinity. Таймаут может использоваться для обнаружения перерывов в поступающих сообщениях.
Если в процессе нет ожидающих сообщений, когда таймаут установлен, и заданное количество миллисекунд проходит без получения сообщения, то handle_info/2 будет вызвано с :timeout в качестве первого аргумента. Таймаут сбрасывается, если ожидающие сообщения или сообщения поступают до истечения таймаута.
Поскольку сообщение может поступить до установки таймаута, даже таймаут в 0 миллисекунд не гарантирует его выполнение. Чтобы немедленно и безусловно выполнить другое действие, используйте инструкцию :continue.
Когда (не) следует использовать GenServer
До сих пор мы узнали, что GenServer можно использовать как контролируемый процесс, обрабатывающий синхронные и асинхронные вызовы. Он также может обрабатывать системные сообщения, такие как периодические сообщения и события мониторинга. Процессы GenServer также могут быть именованными.
GenServer, или процесс в целом, должен использоваться для моделирования временных характеристик вашей системы. GenServer никогда не должен использоваться для организации кода.
В Elixir организация кода выполняется с помощью модулей и функций, процессы не нужны. Например, представьте, что вы реализуете калькулятор и решили поместить все операции калькулятора в GenServer:
def add(a, b) do
GenServer.call(__MODULE__, {:add, 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 по началу работы содержит учебное введение. Документация и ссылки в Erlang также могут предоставить дополнительную информацию.
Резюме
Типы
- debug()
Поддерживаемые параметры отладки для функций
start*- from()
Кортеж, описывающий клиента запроса вызова.
- name()
Имя GenServer
- on_start()
Возвращаемые значения функций
start*- option()
Значения параметров, используемых функциями
start*- options()
Параметры, используемые функциями
start*- server()
Ссылка на сервер.
Функции
- 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если процесс не ассоциирован с заданным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, state)
Вызывается для обработки инструкций
continue.- handle_info(msg, state)
Вызывается для обработки всех других сообщений.
- init(init_arg)
Вызывается при запуске сервера.
start_link/3илиstart/3будут заблокированы, пока не вернётся.- terminate(reason, state)
Вызывается, когда сервер собирается завершить работу. Должен выполнить необходимую очистку.
Типы
debug()
Specs
debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t()}] Поддерживаемые параметры отладки для функций start*
from()
Specs
from() :: {pid(), tag :: term()} Кортеж, описывающий клиента запроса вызова.
pid — PID вызывающего, а tag — уникальный термин, используемый для идентификации вызова.
name()
Specs
name() :: atom() | {:global, term()} | {:via, module(), term()} Имя GenServer
on_start()
Specs
on_start() ::
{:ok, pid()} | :ignore | {:error, {:already_started, pid()} | term()} Возвращаемые значения функций start*
option()
Specs
option() ::
{:debug, debug()}
| {:name, name()}
| {:timeout, timeout()}
| {:spawn_opt, Process.spawn_opt()}
| {:hibernate_after, timeout()} Значения параметров, используемые функциями start*
options()
Specs
options() :: [option()]
Параметры, используемые функциями start*
server()
Specs
server() :: pid() | name() | {atom(), node()} Ссылка на сервер.
Это может быть обычный PID или значение, представляющее зарегистрированное имя. См. раздел «Регистрация имён» в этом документе для получения дополнительной информации.
Функции
abcast(nodes \\ [node() | Node.list()], name, request)
Спецификации
abcast([node()], name :: atom(), term()) :: :abcast
Рассылает сообщение всем серверам, локально зарегистрированным как name на указанных узлах.
Эта функция возвращает значение немедленно и игнорирует узлы, которые не существуют, или где имя сервера не существует.
См. multi_call/4 для получения дополнительной информации.
call(server, request, timeout \\ 5000)
Спецификации
call(server(), term(), timeout()) :: term()
Выполняет синхронный вызов серверу server и ожидает его ответа.
Клиент отправляет заданный request серверу и ожидает, пока не придёт ответ или не наступит таймаут. handle_call/3 будет вызван на сервере для обработки запроса.
server может быть любым из значений, описанных в разделе «Регистрация имён» документации по данному модулю.
Таймауты
timeout — целое число больше нуля, указывающее, сколько миллисекунд ожидать ответа, или атом :infinity для ожидания бесконечно. Значение по умолчанию — 5000. Если ответ не получен в течение указанного времени, вызов функции завершается ошибкой, и вызывающая сторона завершается. Если вызывающая сторона перехватывает ошибку и продолжает работу, а сервер просто опаздывает с ответом, он может прийти в любой момент позже в очередь сообщений вызывающей стороны. В этом случае вызывающая сторона должна быть готова к этому и отбрасывать любые такие сообщения-мусор, которые представляют собой кортежи из двух элементов, где первый элемент — ссылка.
cast(server, request)
Спецификации
cast(server(), term()) :: :ok
Отправляет асинхронный запрос серверу server.
Эта функция всегда возвращает :ok независимо от того, существует ли целевой server (или узел). Поэтому неизвестно, успешно ли целевой server обработал сообщение.
handle_cast/2 будет вызван на сервере для обработки запроса. В случае, если server находится на узле, который ещё не подключен к вызывающей стороне, семантика отличается в зависимости от используемой версии Erlang/OTP.
server может быть любым из значений, описанных в разделе «Регистрация имён» документации по данному модулю.
До Erlang/OTP 21 вызов блокировался до подключения. Это делалось для гарантии порядка. Начиная с Erlang/OTP 21, как Erlang, так и Elixir не блокируют вызов.
multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity)
Спецификации
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 Stack, упомянутый в документации для модуля GenServer, зарегистрирован как Stack на узлах :"foo@my-machine" и :"bar@my-machine":
GenServer.multi_call(Stack, :pop)
#=> {[{:"foo@my-machine", :hello}, {:"bar@my-machine", :world}], []} reply(client, reply)
Спецификации
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 \\ [])
Спецификации
start(module(), any(), options()) :: on_start()
Запускает процесс GenServer без связей (вне дерева контроля).
См. start_link/3 для получения дополнительной информации.
start_link(module, init_arg, options \\ [])
Спецификации
start_link(module(), any(), options()) :: on_start()
Запускает процесс GenServer, связанный с текущим процессом.
Это часто используется для запуска GenServer в рамках дерева контроля.
После запуска сервера вызывается функция init/1 данного module с аргументом init_arg для инициализации сервера. Для обеспечения синхронизированной процедуры запуска эта функция не возвращает значение, пока init/1 не вернёт значение.
Обратите внимание, что GenServer, запущенный с помощью start_link/3, связан с родительским процессом и завершится в случае сбоя родительского процесса. GenServer также завершится по :normal причинам, если он настроен на перехват завершений в обратном вызове 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)
Спецификации
stop(server(), reason :: term(), timeout()) :: :ok
Синхронно останавливает сервер с заданной reason.
Обратный вызов terminate/2 данного server будет вызван перед завершением. Эта функция возвращает :ok если сервер завершается с заданной причиной; если он завершается с другой причиной, вызов завершается ошибкой.
Эта функция сохраняет семантику OTP относительно отчётов об ошибках. Если причина — любая, отличная от :normal, :shutdown или {:shutdown, _}, отчёт об ошибке регистрируется.
whereis(server)
Спецификации
whereis(server()) :: pid() | {atom(), node()} | nil Возвращает pid или {name, node} процесса GenServer, или nil если процесс не связан с данным server.
Примеры
Например, чтобы найти процесс сервера, отслеживать его и отправить ему сообщение:
process = GenServer.whereis(server) monitor = Process.monitor(process) GenServer.cast(process, :hello)
Обработчики
code_change(old_vsn, state, extra)
Спецификации
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)
Спецификации
format_status(reason, pdict_and_state :: list()) :: term() when reason: :normal | :terminate
Вызывается в некоторых случаях для получения отформатированного представления состояния GenServer.
Этот обратный вызов может быть полезен для управления отображением состояния GenServer. Например, он может использоваться для возвращения компактного представления состояния GenServer, чтобы избежать вывода больших структур терминов.
один из
:sys.get_status/1или:sys.get_status/2вызывается для получения состоянияGenServer; в таких случаях,reason—:normalпроцесс
GenServerзавершается аномально и регистрирует ошибку; в таких случаях,reason—:terminate
pdict_and_state — список из двух элементов [pdict, state], где pdict — список кортежей {key, value}, представляющих текущий словарь процесса GenServer, а state — текущее состояние GenServer.
handle_call(request, from, state)
Спецификации
handle_call(request :: term(), from(), state :: term()) ::
{:reply, reply, new_state}
| {:reply, reply, new_state, timeout() | :hibernate | {:continue, term()}}
| {:noreply, new_state}
| {:noreply, new_state, timeout() | :hibernate | {:continue, 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}} аналогично {:reply, reply, new_state}, за исключением того, что handle_continue/2 будет вызван немедленно после этого с значением continue в качестве первого аргумента.
Не следует агрессивно использовать перевод в спящий режим, так как на сборку мусора может уйти много времени. Обычно он используется только тогда, когда сообщение вряд ли придёт в ближайшее время, и минимизация памяти процесса окажется выгодной.
Возвращение {:noreply, new_state} не отправляет ответ отправителю и продолжает цикл с новым состоянием new_state. Ответ необходимо отправить с помощью reply/2.
Существует три основных случая, когда ответ не отправляется с помощью возвращаемого значения:
- Отправить ответ до возвращения из обратного вызова, так как он известен до вызова медленной функции.
- Отправить ответ после возвращения из обратного вызова, так как он ещё недоступен.
- Отправить ответ из другого процесса, например, задачи.
При отправке ответа из другого процесса GenServer должен завершиться, если другой процесс завершится без отправки ответа, так как отправитель будет ожидать ответ.
Возвращение {:noreply, new_state, timeout | :hibernate | {:continue, continue}} аналогично {: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)
Спецификации
handle_cast(request :: term(), state :: term()) ::
{:noreply, new_state}
| {:noreply, new_state, timeout() | :hibernate | {:continue, 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}} аналогично {:noreply, new_state}, за исключением того, что handle_continue/2 будет вызван немедленно после этого с значением continue в качестве первого аргумента.
Возвращение {:stop, reason, new_state} останавливает цикл и вызывает terminate/2 с причиной reason и состоянием new_state. Процесс завершается с причиной reason.
Этот обратный вызов является необязательным. Если он не реализован, сервер завершится ошибкой при использовании отправки.
handle_continue(continue, state)
Спецификации
handle_continue(continue :: term(), state :: term()) ::
{:noreply, new_state}
| {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
| {:stop, reason :: term(), new_state}
when new_state: term() Вызывается для обработки инструкций continue.
Полезна для выполнения задач после инициализации или разделения задач в обратном вызове на несколько шагов, обновляя состояние процесса по ходу выполнения.
Возвращаемые значения аналогичны handle_cast/2.
Этот обратный вызов является необязательным. Если он не реализован, сервер завершится ошибкой при использовании инструкции продолжения.
Этот обратный вызов поддерживается только в Erlang/OTP 21 и выше.
handle_info(msg, state)
Спецификации
handle_info(msg :: :timeout | term(), state :: term()) ::
{:noreply, new_state}
| {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
| {:stop, reason :: term(), new_state}
when new_state: term() Вызывается для обработки всех остальных сообщений.
msg — сообщение, а state — текущее состояние GenServer. При таймауте сообщение — :timeout.
Возвращаемые значения аналогичны handle_cast/2.
Этот обратный вызов является необязательным. Если он не реализован, полученное сообщение будет зарегистрировано.
init(init_arg)
Характеристики
init(init_arg :: term()) ::
{:ok, state}
| {:ok, state, timeout() | :hibernate | {:continue, 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}} аналогичен {:ok, state}, за исключением того, что сразу после входа в цикл будет вызван обратный вызов handle_continue/2 со значением continue в качестве первого аргумента.
Возврат :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)
Характеристики
terminate(reason, state :: term()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()} | term() Вызывается перед завершением сервера. Он должен выполнить все необходимые действия по очистке.
reason — причина завершения, а state — текущее состояние GenServer. Значение возврата игнорируется.
terminate/2 вызывается, если обратный вызов (кроме init/1) выполняет одно из следующих действий:
- возвращает кортеж
:stop - возбуждает исключение
- вызывает
Kernel.exit/1 - возвращает недопустимое значение
- процесс
GenServerловит завершения (используяProcess.flag/2) и родительский процесс отправляет сигнал завершения
Если это часть дерева надзора, GenServer получит сигнал завершения при завершении работы дерева. Сигнал завершения основан на стратегии завершения в спецификации дочернего элемента, где это значение может быть:
:brutal_kill:GenServerубивается, и поэтомуterminate/2не вызывается.значение таймаута, при котором надзор отправит сигнал завершения
:shutdown, иGenServerбудет иметь длительность таймаута для завершения. Если после истечения срока этого таймаута процесс по-прежнему активен, он будет немедленно убит.
Для более подробного объяснения обратитесь к разделу «Значения завершения (:shutdown)» в модуле Supervisor.
Если GenServer получает сигнал завершения (который не :normal) от любого процесса, когда он не ловит завершения, он завершится внезапно с той же причиной и не вызовет terminate/2. Обратите внимание, что процесс НЕ ловит завершения по умолчанию, и сигнал завершения отправляется при завершении связанного процесса или отключении узла.
Поэтому не гарантируется, что terminate/2 будет вызван при завершении GenServer. По этим причинам мы обычно рекомендуем важные правила очистки в отдельных процессах, либо с помощью мониторинга, либо с помощью самих связей. Очистка не требуется, когда GenServer управляет port (например, :gen_tcp.socket) или File.io_device/0, потому что они будут закрыты при получении сигнала завершения GenServer и не нуждаются в ручном закрытии в terminate/2.
Если reason не является ни :normal, ни :shutdown, ни {:shutdown, term}, регистрируется ошибка.
Этот обратный вызов является необязательным.
© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.10.4/GenServer.html