Spec-Zone.ru › Elixir 1.5

GenServer поведение

Модуль поведения для реализации сервера в клиент-серверном взаимодействии.

GenServer — это процесс, подобный любому другому процессу Elixir, и он может использоваться для хранения состояния, выполнения кода асинхронно и т. д. Преимущество использования универсального серверного процесса (GenServer), реализованного с помощью этого модуля, состоит в том, что у него будет стандартный набор интерфейсных функций и функциональность для отслеживания и обработки ошибок. Он также будет подходить для размещения в дереве надзора.

Пример

Поведение GenServer абстрагирует общее взаимодействие клиент-сервер. Разработчикам необходимо только реализовать необходимые обратные вызовы и функциональность.

Давайте начнём с примера кода, а затем рассмотрим доступные обратные вызовы. Представим, что нам нужен GenServer, который работает как стек, позволяющий нам помещать и извлекать элементы:

defmodule Stack do
  use GenServer

  # Callbacks

  def handle_call(:pop, _from, [h | t]) do
    {:reply, h, t}
  end

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

  def handle_call(:pop, _from, [h | t]) do
    {:reply, h, t}
  end

  def handle_call(request, from, state) do
    # Call the default implementation from GenServer
    super(request, from, state)
  end

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

  def handle_cast(request, state) do
    super(request, 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

  def init(state) do
    schedule_work() # Schedule work to be performed on start
    {:ok, state}
  end

  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]}]]]}

Дополнительная информация

Если вы хотите узнать больше о серверах gen, руководство Elixir по началу работы предоставляет учебное введение. Документация и ссылки в 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()} when old_vsn: term() | {:down, term()}

Вызывается для изменения состояния GenServer при загрузке другой версии модуля (горячая замена кода), и структура данных состояния должна быть изменена.

old_vsn — предыдущая версия модуля (определённая атрибутом @vsn) при обновлении. При понижении предыдущая версия упаковывается в 2-кортеж с первым элементом :down. state — текущее состояние GenServer, а extra — любые дополнительные данные, необходимые для изменения состояния.

Возврат {:ok, new_state} изменяет состояние на new_state и изменение кода выполняется успешно.

Возврат {:error, reason} приводит к отказу изменения кода с причиной reason, и состояние остаётся прежним.

Если code_change/3 вызывает исключение, изменение кода терпит неудачу, и цикл продолжится с предыдущим состоянием. Поэтому этот обратный вызов обычно не содержит побочных эффектов.

format_status(reason, pdict_and_state) (необязательно)

format_status(reason, pdict_and_state :: list()) :: term() when reason: :normal | :terminate

Вызывается в некоторых случаях для получения отформатированной версии состояния GenServer.

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

  • вызывается один из :sys.get_status/1 или :sys.get_status/2 для получения статуса GenServer; в таких случаях reason имеет значение :normal

  • GenServer завершается аварийно и регистрируется ошибка; в таких случаях reason имеет значение :terminate

pdict_and_state — список из двух элементов [pdict, state], где pdict — список кортежей {key, value}, представляющих текущий словарь процесса GenServer, а state — текущее состояние GenServer.

handle_call(request, from, state)

handle_call(request :: term(), from(), state :: term()) ::
  {:reply, reply, new_state} |
  {:reply, reply, new_state, timeout() | :hibernate} |
  {: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 — 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 вызывает сборку мусора и оставляет непрерывную кучу, что минимизирует используемую память процессом.

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

Возврат {: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 вернёт {:stop, {:bad_call, request}, state}.

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 вернёт {:stop, {:bad_cast, request}, state}.

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()} | term()

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

reason — причина завершения, а state — текущее состояние GenServer. Значение возврата игнорируется.

terminate/2 вызывается, если обратный вызов (кроме init/1) выполняет одно из следующих действий:

  • возвращает кортеж :stop
  • вызывает исключение
  • вызывает Kernel.exit/1
  • возвращает недопустимое значение
  • сервер GenServer перехватывает завершения (используя Process.flag/2) и родительский процесс отправляет сигнал завершения

Если сервер GenServer входит в дерево наблюдения, его 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.5.3/GenServer.html

Spec-Zone.ru

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