Spec-Zone.ru › Elixir 1.7

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.

Клиент / Сервер 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 и обратных вызовов

Существует 7 обратных вызовов, которые необходимо реализовать при использовании GenServer. Единственный обязательный обратный вызов — init/1.

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.

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

Цель GenServer — абстрагировать цикл «получение» для разработчиков, автоматически обрабатывая системные сообщения, поддерживая изменения кода, синхронные вызовы и многое другое. Поэтому вы никогда не должны вызывать свой собственный «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

Когда не следует использовать 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},            # 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 для начинающих предоставляет вводный урок. Документация и ссылки в Erlang также могут предоставить дополнительную информацию.

  • GenServer – Руководство Elixir по началу работы
  • :gen_server документация модуля
  • Поведение gen_server – Принципы проектирования 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, 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_continue(continue, state)

Вызывается для обработки инструкций continue.

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 — используется для регистрации имени, как описано в разделе «Регистрация имен» в документации по GenServer

  • :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 | {: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} за исключением того, что 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) (optional)

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) (optional)

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.

Этот коллбэк является необязательным. Если он не реализован, сервер завершится ошибкой при использовании инструкции continue.

Этот коллбэк поддерживается только в Erlang/OTP 21+.

handle_info(msg, state) (optional)

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(args)

init(args :: term()) ::
  {:ok, state}
  | {:ok, state, timeout() | :hibernate | {:continue, term()}}
  | :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 для получения дополнительной информации о приостановке.

Возвращение {: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) (optional)

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 отправит сигнал завершения при его выключении. Сигнал завершения зависит от стратегии завершения в описании дочернего процесса. Если это :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.7.4/GenServer.html

Spec-Zone.ru

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