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, item}, state) do
{:noreply, [item | 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, item) do
GenServer.cast(pid, {:push, item})
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, item}, state) do
{:noreply, [item | 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— идентификатор спецификации подпроцесса, по умолчанию равен текущему модулю -
:start— способ запуска процесса подпроцесса (по умолчанию вызывается__MODULE__.start_link/1) -
: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 Когда (не) следует использовать 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 Getting Started предоставляет обучающий вводный курс. Документация и ссылки в Erlang также могут предоставить дополнительную информацию.
- GenServer — Руководство Elixir по началу работы
-
:gen_serverдокументация по модулю - Gen_server Behaviour — Принципы проектирования OTP
- Клиенты и серверы — Учитесь 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()
debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t()}] Поддерживаемые параметры отладки для функций start*
from()
from() :: {pid(), tag :: term()} Кортеж, описывающий клиента запроса вызова.
pid — PID вызывающего, а tag — уникальный термин, используемый для идентификации вызова.
name()
name() :: atom() | {:global, term()} | {:via, module(), term()} Имя GenServer
on_start()
on_start() ::
{:ok, pid()} | :ignore | {:error, {:already_started, pid()} | term()} Возвращаемые значения функций start*
option()
option() ::
{:debug, debug()}
| {:name, name()}
| {:timeout, timeout()}
| {:spawn_opt, Process.spawn_opt()}
| {:hibernate_after, timeout()} Значения параметров, используемые функциями start*
options()
options() :: [option()]
Параметры, используемые функциями start*
server()
server() :: pid() | name() | {atom(), node()} Ссылка на сервер
Функции
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.
Перед 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 — это список имён узлов, которым отправляется запрос. По умолчанию используется список всех известных узлов (включая этот узел).
Чтобы избежать того, что поздние ответы (после таймаута) загрязняют очередь сообщений вызывающей стороны, используется процесс-посредник для фактических вызовов. Поздние ответы затем будут отброшены при получении процессом, который завершил работу.
Примеры
Предполагая, что Stack 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)
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- если присутствует, серверу разрешается потратить указанное количество миллисекунд на инициализацию, иначе он будет завершен, и функция start вернет{: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()} | {:down, term()}
when old_vsn: 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 — это кортеж из двух элементов, содержащий PID вызывающего процесса и уникальный идентификатор вызова, а state — текущее состояние GenServer.
Возврат {:reply, reply, new_state} отправляет ответ reply вызывающему процессу и продолжает цикл с новым состоянием new_state.
Возврат {:reply, reply, new_state, timeout} аналогичен {:reply, reply, new_state} за исключением того, что handle_info(:timeout, new_state) будет вызван через timeout миллисекунд, если сообщения не получены.
Возврат {: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} за исключением того, что handle_info(:timeout, new_state) будет вызван через timeout миллисекунд, если сообщения не получены.
Возврат {: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.
Этот обратный вызов является необязательным. Если он не реализован, сервер завершится ошибкой, если будет выполнен вызов cast.
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} за исключением того, что handle_info(:timeout, state) будет вызван через timeout миллисекунд, если сообщение не получено в течение таймаута.
Возврат {: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()} Вызывается, когда сервер собирается завершиться. Он должен выполнить все необходимые действия по очистке.
reason — причина завершения, а state — текущее состояние GenServer. Значение возврата игнорируется.
terminate/2 вызывается, если обратный вызов (кроме init/1) выполняет одно из следующих действий:
- возвращает кортеж
:stop - вызывает исключение
- вызывает
Kernel.exit/1 - возвращает недопустимое значение
- процесс ловит завершения (с использованием
Process.flag/2) и родительский процесс отправляет сигнал завершения
Если процесс является частью дерева наблюдения, он получит сигнал завершения при завершении работы дерева. Сигнал завершения основан на стратегии завершения в спецификации дочернего процесса, где это значение может быть:
-
: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.8.2/GenServer.html