Spec-Zone.ru › Elixir 1.6

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, [h | t]) do
    {:reply, h, t}
  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/3, передавая модуль с реализацией сервера и его начальный аргумент (список, представляющий стек, содержащий элемент :hello). Мы можем в основном взаимодействовать с сервером, отправляя два типа сообщений. Сообщения типа call ожидают ответа от сервера (и поэтому являются синхронными), в то время как сообщения типа cast не ожидают ответа.

Каждый раз, когда вы делаете GenServer.call/3, клиент отправляет сообщение, которое должно обрабатываться обратным вызовом handle_call/3 в GenServer. Сообщение типа cast/2 должно обрабатываться обратным вызовом handle_cast/2.

Использование GenServer и обратных вызовов

Для реализации в GenServer необходимо реализовать 6 обратных вызовов. Добавив use GenServer в свой модуль, Elixir автоматически определит все 6 обратных вызовов, оставив вам возможность реализовать те, которые вы хотите настроить.

use GenServer также определяет функцию child_spec/1, позволяющую поместить определённый модуль в дерево надзора. Сгенерированный child_spec/1 можно настроить с помощью следующих опций:

  • :id - идентификатор спецификации подпроцесса, по умолчанию равен текущему модулю
  • :start - как запустить подпроцесс (по умолчанию вызывается __MODULE__.start_link/1)
  • :restart - когда подпроцесс должен быть перезапущен, по умолчанию :permanent
  • :shutdown - как остановить подпроцесс

Например:

use GenServer, restart: :transient, shutdown: 10_000

См. документацию 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, который использует эти функции для хранения списка имён процессов и их соответствующих идентификаторов процесса (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.

Клиентский/серверный API

Хотя в примере выше мы использовали GenServer.start_link/3 и другие функции для прямого запуска и взаимодействия с сервером, большинство времени мы не вызываем функции GenServer напрямую. Вместо этого мы оборачиваем вызовы в новые функции, представляющие публичный API сервера.

Вот более удачная реализация нашего модуля Stack:

defmodule Stack do
  use GenServer

  # Client

  def start_link(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 handle_call(:pop, _from, [h | t]) do
    {:reply, h, t}
  end

  @impl true
  def handle_cast({:push, item}, state) do
    {:noreply, [item | state]}
  end
end

На практике часто бывает так, что функции сервера и клиента находятся в одном модуле. Если реализации сервера и/или клиента становятся сложными, вы можете поместить их в разные модули.

Получение «обычных» сообщений

Цель GenServer — абстрагировать цикл «receive» для разработчиков, автоматически обрабатывая системные сообщения, поддерживая изменения кода, синхронные вызовы и многое другое. Поэтому вы никогда не должны вызывать собственный «receive» внутри обратных вызовов 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() # Schedule work to be performed on start
    {:ok, state}
  end

  @impl true
  def handle_info(:work, state) do
    # Do the desired work here
    schedule_work() # Reschedule once more
    {:noreply, state}
  end

  defp schedule_work() do
    Process.send_after(self(), :work, 2 * 60 * 60 * 1000) # In 2 hours
  end
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},            # pdict
   "$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]}]]]}

Узнать больше

Если вы хотите узнать больше о GenServer, руководство Elixir Getting Started предоставляет учебное введение. Документация и ссылки в Erlang также могут предоставить дополнительную информацию.

  • GenServer — Руководство Elixir по началу работы
  • Документация модуля :gen_server
  • Поведение gen_server — Принципы проектирования OTP
  • Клиенты и серверы — Learn You Some Erlang for Great Good!

Резюме

Типы

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, args, options \\ [])

Запускает процесс GenServer без связей (вне дерева надзора).

start_link(module, args, 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_info(msg, state)

Вызывается для обработки всех остальных сообщений.

init(args)

Вызывается при запуске сервера. 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()}

Значения опций, используемые функциями 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 находится на узле, который еще не подключен к вызывающей стороне, вызов будет заблокирован до подключения. Это отличается от поведения в OTP's :gen_server, где сообщение в этом случае отправляется другим процессом, что может привести к тому, что сообщения на другие узлы поступят в неправильном порядке.

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, args, options \\ [])

start(module(), any(), options()) :: on_start()

Запускает процесс GenServer без связей (вне дерева наблюдения).

См. start_link/3 для получения дополнительной информации.

start_link(module, args, options \\ [])

start_link(module(), any(), options()) :: on_start()

Запускает процесс GenServer, связанный с текущим процессом.

Это часто используется для запуска GenServer как части дерева наблюдения.

После запуска сервера вызывается функция init/1 заданного module с аргументами args для инициализации сервера. Для обеспечения синхронизированной процедуры запуска эта функция не возвращает значение, пока init/1 не вернёт значение.

Обратите внимание, что GenServer, запущенный с помощью start_link/3, связан с родительским процессом и завершится в случае сбоя родительского процесса. GenServer также завершится по причинам :normal, если он настроен на перехват завершений в обратном вызове init/1.

Параметры

  • :name - используется для регистрации имени, как описано в разделе «Регистрация имени» документации модуля

  • :timeout - если указано, серверу разрешено потратить заданное количество миллисекунд на инициализацию, в противном случае он будет завершен, и функция start вернёт {:error, :timeout}

  • :debug - если указано, вызывается соответствующая функция в модуле :sys

  • :spawn_opt - если указано, его значение передаётся в качестве параметров подлежащему процессу, как в Process.spawn/4

Возвращаемые значения

Если сервер успешно создан и инициализирован, эта функция возвращает {: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}
  | {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate}
  | {: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 вызывает сборку мусора и оставляет непрерывную кучу, которая минимизирует используемую память процессом.

Не следует агрессивно использовать приостановку, так как может быть затрачено слишком много времени на сборку мусора. Обычно она должна использоваться только тогда, когда сообщение не ожидается в ближайшее время, и минимизация памяти процесса оказывается полезной.

Возвращение {:noreply, new_state} не отправляет ответ отправителю и продолжает цикл с новым состоянием new_state. Ответ должен быть отправлен с помощью reply/2.

Существует три основных случая, когда не нужно отвечать, используя возвращаемое значение:

  • Отправить ответ до возвращения из обратного вызова, потому что ответ известен до вызова медленной функции.
  • Отправить ответ после возвращения из обратного вызова, потому что ответ ещё недоступен.
  • Отправить ответ из другого процесса, например, задачи.

При ответе из другого процесса, GenServer должен завершиться, если другой процесс завершится без ответа, так как отправитель будет заблокирован, ожидая ответа.

Возвращение {:noreply, new_state, timeout | :hibernate} аналогично {: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}, за исключением того, что ответ не отправляется.

Если этот обратный вызов не реализован, реализация по умолчанию use GenServer завершится ошибкой RuntimeError с сообщением: попытка вызвать GenServer, но ни один вариант handle_call/3 не был предоставлен.

handle_cast(request, state)

handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate}
  | {: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.

Возвращение {:stop, reason, new_state} останавливает цикл и вызывает terminate/2 с причиной reason и состоянием new_state. Процесс завершается с причиной reason.

Если эта обратная функция не реализована, то по умолчанию реализация от use GenServer завершится ошибкой с исключением RuntimeError с сообщением: попытка вызвать GenServer, но не указан handle_cast/2.

handle_info(msg, state)

handle_info(msg :: :timeout | term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate}
  | {:stop, reason :: term(), new_state}
when new_state: term()

Вызывается для обработки всех остальных сообщений.

msg — это сообщение, а state — текущее состояние GenServer. При возникновении таймаута сообщение — :timeout.

Значения возврата совпадают со значениями возврата handle_cast/2.

Если эта обратная функция не реализована, по умолчанию реализация от use GenServer вернёт {:noreply, state}.

init(args)

init(args :: term()) ::
  {:ok, state}
  | {:ok, state, timeout() | :hibernate}
  | :ignore
  | {:stop, reason :: any()}
when state: any()

Вызывается при запуске сервера. start_link/3 или start/3 будут блокироваться до возврата.

args — это аргумент (второй аргумент), переданный в 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.

Возвращение :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
  • возвращает недопустимое значение
  • GenServer перехватывает завершения (используя Process.flag/2) и родительский процесс отправляет сигнал завершения

Если это часть дерева контроля, GenServer’s Supervisor отправит сигнал завершения при его завершении. Сигнал завершения основан на стратегии завершения в спецификации дочернего элемента. Если это :brutal_kill, GenServer убивается, и terminate/2 не вызывается. Однако, если это таймаут, Supervisor отправит сигнал завершения :shutdown, и GenServer будет иметь время таймаута, чтобы вызвать terminate/2 — если процесс всё ещё жив после таймаута, он убивается.

Если 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.6.6/GenServer.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API