Spec-Zone.ru › Elixir 1.9

GenServer поведение

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

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

Пример

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

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

defmodule Stack do
  use GenServer

  # Callbacks

  @impl true
  def init(stack) do
    {:ok, stack}
  end

  @impl true
  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

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

# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])

# This is the client
GenServer.call(pid, :pop)
#=> :hello

GenServer.cast(pid, {:push, :world})
#=> :ok

GenServer.call(pid, :pop)
#=> :world

Мы запускаем наш Stack вызовом start_link/2, передавая модуль с реализацией сервера и его начальный аргумент (список, представляющий стек, содержащий элемент :hello). Мы можем в основном взаимодействовать с сервером, отправляя два типа сообщений. Сообщения call ожидают ответа от сервера (и поэтому являются синхронными), а сообщения cast — нет.

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

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

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

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

defmodule Stack do
  use GenServer

  # Client

  def start_link(default) when is_list(default) do
    GenServer.start_link(__MODULE__, default)
  end

  def push(pid, element) do
    GenServer.cast(pid, {:push, element})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  # Server (callbacks)

  @impl true
  def init(stack) do
    {:ok, stack}
  end

  @impl true
  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

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

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

Как контролировать

GenServer обычно запускается в дереве управления. Когда мы вызываем use GenServer, он автоматически определяет функцию child_spec/1, которая позволяет нам запустить Stack непосредственно под надзором. Чтобы запустить стандартный стек [:hello] под надзором, можно сделать так:

children = [
  {Stack, [:hello]}
]

Supervisor.start_link(children, strategy: :one_for_all)

Обратите внимание, что вы также можете запустить его просто как Stack, что эквивалентно {Stack, []}:

children = [
  Stack # The same as {Stack, []}
]

Supervisor.start_link(children, strategy: :one_for_all)

В обоих случаях Stack.start_link/1 всегда вызывается.

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

  • :id — идентификатор спецификации дочернего элемента, по умолчанию равен текущему модулю
  • :restart — когда дочерний элемент должен быть перезапущен, по умолчанию :permanent
  • :shutdown — как остановить дочерний элемент, немедленно или предоставив ему время на остановку

Например:

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

См. раздел «Спецификация дочернего элемента» в модуле Supervisor для получения более подробной информации. Аннотация @doc непосредственно перед use GenServer будет добавлена к сгенерированной функции child_spec/1.

Регистрация имени

Как start_link/3, так и start/3 поддерживают GenServer для регистрации имени при запуске через опцию :name. Зарегистрированные имена также автоматически очищаются при завершении работы. Поддерживаемые значения:

  • атом — GenServer регистрируется локально с заданным именем с помощью Process.register/2.

  • {:global, term} — GenServer регистрируется глобально с заданным значением с помощью функций модуля :global.

  • {:via, module, term} — GenServer регистрируется с заданным механизмом и именем. Опция :via ожидает модуль, который экспортирует register_name/2, unregister_name/1, whereis_name/1 и send/2. Пример — модуль :global, который использует эти функции для хранения списка имен процессов и их связанных идентификаторов процесса (PID), доступных глобально для сети узлов Elixir. Elixir также поставляется с локальным, децентрализованным и масштабируемым реестром, называемым Registry, для локального хранения динамически генерируемых имён.

Например, мы можем запустить и зарегистрировать наш Stack сервер локально следующим образом:

# Start the server and register it locally with name MyStack
{:ok, _} = GenServer.start_link(Stack, [:hello], name: MyStack)

# Now messages can be sent directly to MyStack
GenServer.call(MyStack, :pop)
#=> :hello

После запуска сервера оставшиеся функции в этом модуле (call/3, cast/2 и другие) также будут принимать атом или любые {:global, ...} или {:via, ...} кортежи. В общем, поддерживаются следующие форматы:

  • Идентификатор процесса (PID)
  • Атом, если сервер зарегистрирован локально
  • {atom, node} если сервер зарегистрирован локально на другом узле
  • {:global, term} если сервер зарегистрирован глобально
  • {:via, module, name} если сервер зарегистрирован через альтернативный реестр

Если есть необходимость зарегистрировать динамические имена локально, не используйте атомы, так как атомы никогда не удаляются сборщиком мусора, а поэтому динамически сгенерированные атомы не будут удалены. В таких случаях можно создать собственный локальный реестр, используя модуль Registry.

Прием "обычных" сообщений

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

Помимо синхронной и асинхронной коммуникации, обеспечиваемой call/3 и cast/2, «обычные» сообщения, отправленные функциями, такими как Kernel.send/2, Process.send_after/4 и аналогичными, могут обрабатываться внутри обратного вызова handle_info/2.

handle_info/2 может использоваться во многих ситуациях, таких как обработка сообщений DOWN, отправленных Process.monitor/1. Другой вариант использования handle_info/2 — выполнять периодическую работу с помощью Process.send_after/4:

defmodule MyApp.Periodically do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{})
  end

  @impl true
  def init(state) do
    # Schedule work to be performed on start
    schedule_work()

    {:ok, state}
  end

  @impl true
  def handle_info(:work, state) do
    # Do the desired work here
    # ...

    # Reschedule once more
    schedule_work()

    {:noreply, state}
  end

  defp schedule_work do
    # In 2 hours
    Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
  end
end

Таймауты

Возвращаемое значение init/1 или любого из обратных вызовов handle_* может включать значение таймаута в миллисекундах; если нет, предполагается :infinity.

Если в процессе нет ожидающих сообщений, когда устанавливается таймаут, и заданное количество миллисекунд проходит без поступления сообщения, тогда handle_info/2 будет вызван с :timeout в качестве первого аргумента. Таймаут сбрасывается, если ожидающее сообщение или сообщение поступает до заданного таймаута.

Поскольку сообщение может поступить до установки таймаута, даже таймаут в 0 миллисекунд не гарантирует его выполнение. Чтобы немедленно и безоговорочно выполнить другую операцию, используйте инструкцию :continue.

Когда не стоит использовать GenServer

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

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

В Elixir организация кода выполняется с помощью модулей и функций, процессы не нужны. Например, представьте, что вы реализуете калькулятор, и решите поместить все операции калькулятора в GenServer:

def add(a, b) do
  GenServer.call(__MODULE__, {:add, a, b})
end

def handle_call({:add, a, b}, _from, state) do
  {:reply, a + b, state}
end

def handle_call({:subtract, a, b}, _from, state) do
  {:reply, a - b, state}
end

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

def add(a, b) do
  a + b
end

def subtract(a, b) do
  a - b
end

Если вам не нужен процесс, то вам не нужен процесс. Используйте процессы только для моделирования временных характеристик, таких как изменяемое состояние, конкурентность и ошибки, но никогда для организации кода.

Отладка с использованием модуля :sys

GenServer, как специальные процессы, могут быть отлажены с помощью модуля :sys. С помощью различных хуков этот модуль позволяет разработчикам исследовать состояние процесса и отслеживать системные события, происходящие во время его выполнения, такие как полученные сообщения, отправленные ответы и изменения состояния.

Давайте рассмотрим основные функции модуля :sys, используемые для отладки:

  • :sys.get_state/2 - позволяет получить состояние процесса. В случае процесса GenServer, это будет состояние модуля обратного вызова, переданное в функции обратного вызова в качестве последнего аргумента.
  • :sys.get_status/2 - позволяет получить статус процесса. Этот статус включает словарь процесса, запущен ли процесс или приостановлен, родительский PID, состояние отладчика и состояние модуля поведения, которое включает состояние модуля обратного вызова (как возвращается функцией :sys.get_state/2). Можно изменить представление этого статуса, определив необязательный GenServer.format_status/2 обратный вызов.
  • :sys.trace/3 - печатает все системные события в :stdio.
  • :sys.statistics/3 - управляет сбором статистики процессов.
  • :sys.no_debug/2 - отключает все обработчики отладки для данного процесса. Очень важно отключить отладку после завершения работы. Избыточные обработчики отладки или те, которые должны быть отключены, но не были, могут серьезно ухудшить производительность системы.
  • :sys.suspend/2 - позволяет приостановить процесс, чтобы он отвечал только на системные сообщения, но не на другие. Приостановленный процесс может быть возобновлен с помощью :sys.resume/2.

Давайте посмотрим, как мы можем использовать эти функции для отладки сервера стека, который мы определили ранее.

iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
*DBG* <0.122.0> got cast {push,1}
*DBG* <0.122.0> new state [1]
:ok

iex> :sys.get_state(pid)
[1]

iex> Stack.pop(pid)
*DBG* <0.122.0> got call pop from <0.80.0>
*DBG* <0.122.0> sent 1 to <0.80.0>, new state []
1

iex> :sys.statistics(pid, :get)
{:ok,
 [
   start_time: {{2016, 7, 16}, {12, 29, 41}},
   current_time: {{2016, 7, 16}, {12, 29, 50}},
   reductions: 117,
   messages_in: 2,
   messages_out: 0
 ]}

iex> :sys.no_debug(pid) # turn off all debug handlers
:ok

iex> :sys.get_status(pid)
{:status, #PID<0.122.0>, {:module, :gen_server},
 [
   [
     "$initial_call": {Stack, :init, 1},            # process dictionary
     "$ancestors": [#PID<0.80.0>, #PID<0.51.0>]
   ],
   :running,                                        # :running | :suspended
   #PID<0.80.0>,                                    # parent
   [],                                              # debugger state
   [
     header: 'Status for generic server <0.122.0>', # module status
     data: [
       {'Status', :running},
       {'Parent', #PID<0.80.0>},
       {'Logged events', []}
     ],
     data: [{'State', [1]}]
   ]
 ]}

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

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

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

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

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

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

stop(server, reason \\ :normal, timeout \\ :infinity)

Синхронно останавливает сервер с указанной reason.

whereis(server)

Возвращает pid или {name, node} процесса GenServer, или nil если процесс не ассоциирован с данным server.

Обратные вызовы

code_change(old_vsn, state, extra)

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

format_status(reason, pdict_and_state)

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

handle_call(request, from, state)

Вызывается для обработки синхронных сообщений call/3. call/3 будет заблокирован до получения ответа (если вызов не истечет или узлы не будут отключены).

handle_cast(request, state)

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

handle_continue(continue, state)

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

handle_info(msg, state)

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

init(init_arg)

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

terminate(reason, state)

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

Типы

debug()

Specs

debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t()}]

Поддерживаемые параметры отладки для функций start*

from()

Specs

from() :: {pid(), tag :: term()}

Кортеж, описывающий клиента запроса вызова.

pid — PID вызывающего лица, а tag — уникальное значение, используемое для идентификации вызова.

name()

Specs

name() :: atom() | {:global, term()} | {:via, module(), term()}

Имя GenServer

on_start()

Specs

on_start() ::
  {:ok, pid()} | :ignore | {:error, {:already_started, pid()} | term()}

Значения, возвращаемые функциями start*

option()

Specs

option() ::
  {:debug, debug()}
  | {:name, name()}
  | {:timeout, timeout()}
  | {:spawn_opt, Process.spawn_opt()}
  | {:hibernate_after, timeout()}

Значения параметров, используемые функциями start*

options()

Specs

options() :: [option()]

Параметры, используемые функциями start*

server()

Specs

server() :: pid() | name() | {atom(), node()}

Ссылка на сервер.

Это либо обычный PID, либо значение, представляющее зарегистрированное имя. Более подробная информация содержится в разделе "Регистрация имен" этого документа.

Функции

abcast(nodes \\ [node() | Node.list()], name, request)

Спецификации

abcast([node()], name :: atom(), term()) :: :abcast

Рассылает сообщение всем серверам, локально зарегистрированным как name на указанных узлах.

Функция возвращает результат немедленно и игнорирует узлы, которые не существуют, или где имя сервера отсутствует.

Для получения дополнительной информации см. multi_call/4.

call(server, request, timeout \\ 5000)

Спецификации

call(server(), term(), timeout()) :: term()

Выполняет синхронный вызов на server и ожидает ответа.

Клиент отправляет указанный request серверу и ожидает получения ответа или наступления таймаута. На сервере будет вызван метод handle_call/3 для обработки запроса.

server может быть любым из значений, описанных в разделе «Регистрация имен» документации по данному модулю.

Таймауты

timeout — целое число больше нуля, определяющее время ожидания ответа в миллисекундах, или атом :infinity для неограниченного ожидания. Значение по умолчанию — 5000. Если ответ не получен в течение заданного времени, вызов функции завершается ошибкой, и вызывающий процесс завершается. Если вызывающий процесс перехватывает ошибку и продолжает работу, а сервер лишь немного запаздывает с ответом, ответ может поступить в очередь сообщений вызывающего процесса в любой момент позже. В этом случае вызывающий процесс должен быть готов к этому и отбрасывать любые такие мусорные сообщения, которые являются кортежами из двух элементов, где первый элемент — ссылка.

cast(server, request)

Спецификации

cast(server(), term()) :: :ok

Отправляет асинхронный запрос на server.

Эта функция всегда возвращает :ok независимо от того, существует ли целевой server (или узел). Поэтому неизвестно, успешно ли целевой server обработало сообщение.

handle_cast/2 будет вызван на сервере для обработки запроса. В случае, если server находится на узле, который еще не подключен к вызывающему процессу, семантика отличается в зависимости от используемой версии Erlang/OTP.

server может быть любым из значений, описанных в разделе «Регистрация имен» документации по данному модулю.

До Erlang/OTP 21 вызов блокировался до подключения. Это было сделано для гарантии порядка. Начиная с Erlang/OTP 21, как Erlang, так и Elixir не блокируют вызов.

multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity)

Спецификации

multi_call([node()], name :: atom(), term(), timeout()) ::
  {replies :: [{node(), term()}], bad_nodes :: [node()]}

Вызывает все серверы, локально зарегистрированные как name на указанных nodes.

Сначала request отправляется каждому узлу в nodes; затем вызывающий процесс ожидает ответы. Функция возвращает кортеж из двух элементов {replies, bad_nodes}, где:

  • replies — список кортежей {node, reply}, где node — узел, который ответил, а reply — его ответ
  • bad_nodes — список узлов, которые либо не существовали, либо на которых сервер с заданным name не существовал или не ответил

nodes — список имён узлов, которым отправляется запрос. По умолчанию — список всех известных узлов (включая текущий узел).

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

Примеры

Предполагая, что Stack GenServer, упомянутый в документации по модулю GenServer, зарегистрирован как Stack на узлах :"foo@my-machine" и :"bar@my-machine":

GenServer.multi_call(Stack, :pop)
#=> {[{:"foo@my-machine", :hello}, {:"bar@my-machine", :world}], []}

reply(client, reply)

Спецификации

reply(from(), term()) :: :ok

Отправляет ответ клиенту.

Эта функция может использоваться для явного отправления ответа клиенту, который вызывал call/3 или multi_call/4, когда ответ не может быть указан в значении возврата handle_call/3.

client должен быть аргументом from, принятым обратными вызовами handle_call/3. reply — произвольное значение, которое будет возвращено клиенту как результат вызова.

Обратите внимание, что reply/2 может быть вызван из любого процесса, а не только GenServer, который первоначально получил вызов (если этот GenServer каким-то образом передал аргумент from).

Эта функция всегда возвращает :ok.

Примеры

def handle_call(:reply_in_one_second, from, state) do
  Process.send_after(self(), {:reply, from}, 1_000)
  {:noreply, state}
end

def handle_info({:reply, from}, state) do
  GenServer.reply(from, :one_second_has_passed)
  {:noreply, state}
end

start(module, init_arg, options \\ [])

Спецификации

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

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

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

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

Спецификации

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

Запускает процесс GenServer со связью с текущим процессом.

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

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

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

Параметры

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

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

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

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

  • :hibernate_after — если присутствует, процесс GenServer ожидает любое сообщение в течение заданного количества миллисекунд. Если сообщение не получено, процесс автоматически переходит в режим ожидания (вызывая :proc_lib.hibernate/3).

Значения возврата

Если сервер успешно создан и инициализирован, функция возвращает {:ok, pid}, где pid — PID сервера. Если процесс с указанным именем сервера уже существует, функция возвращает {:error, {:already_started, pid}} с PID этого процесса.

Если обратный вызов init/1 завершается ошибкой reason, функция возвращает {:error, reason}. В противном случае, если он возвращает {:stop, reason} или :ignore, процесс завершается, а функция возвращает {:error, reason} или :ignore соответственно.

stop(server, reason \\ :normal, timeout \\ :infinity)

Спецификации

stop(server(), reason :: term(), timeout()) :: :ok

Синхронно останавливает сервер с указанным reason.

Обратный вызов terminate/2 данного server будет вызван перед завершением. Функция возвращает :ok если сервер завершается с указанной причиной; если он завершается по другой причине, вызов завершается.

Эта функция поддерживает семантику OTP относительно отчётности об ошибках. Если причина — любая, кроме :normal, :shutdown или {:shutdown, _}, регистрируется отчёт об ошибке.

whereis(server)

Спецификации

whereis(server()) :: pid() | {atom(), node()} | nil

Возвращает pid или {name, node} процесса GenServer, или nil если процесс, связанный с данным server, не найден.

Примеры

Например, для поиска процесса сервера, мониторинга его и отправки ему асинхронного сообщения:

process = GenServer.whereis(server)
monitor = Process.monitor(process)
GenServer.cast(process, :hello)

Обработчики событий

code_change(old_vsn, state, extra)

Спецификации

code_change(old_vsn, state :: term(), extra :: term()) ::
  {:ok, new_state :: term()} | {:error, reason :: term()}
when old_vsn: term() | {:down, term()}

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

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

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

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

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

Этот обратчик необязателен.

format_status(reason, pdict_and_state)

Спецификации

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

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

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

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

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

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

handle_call(request, from, state)

Спецификации

handle_call(request :: term(), from(), state :: term()) ::
  {:reply, reply, new_state}
  | {:reply, reply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason, reply, new_state}
  | {:stop, reason, new_state}
when reply: term(), new_state: term(), reason: term()

Вызывается для обработки синхронных сообщений call/3. call/3 будет ожидать ответа (если вызов не истечёт и узлы не отключатся).

request — сообщение запроса, отправленное call/3, from — кортеж из двух элементов, содержащий PID вызывающей стороны и уникальный идентификатор запроса, а state — текущее состояние GenServer.

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

Возврат {:reply, reply, new_state, timeout} аналогичен {:reply, reply, new_state}, но также устанавливает таймаут. Более подробную информацию см. в разделе "Таймауты" в документации модуля.

Возврат {:reply, reply, new_state, :hibernate} аналогичен {:reply, reply, new_state}, но процесс переводится в спящее состояние и продолжит цикл, как только сообщение появится в очереди сообщений. Если сообщение уже находится в очереди, это произойдёт немедленно. Перевод GenServer в спящее состояние вызывает сборку мусора и оставляет непрерывную кучу, что сводит к минимуму используемую память процессом.

Возврат {:reply, reply, new_state, {:continue, continue}} аналогичен {:reply, reply, new_state}, но handle_continue/2 будет вызвана немедленно после этого со значением continue в качестве первого аргумента.

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

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

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

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

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

Возврат {:noreply, new_state, timeout | :hibernate | {:continue, continue}} аналогичен {:noreply, new_state}, за исключением таймаута, приостановки или продолжения, как и в случае кортежа :reply.

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

Возврат {:stop, reason, new_state} аналогичен {:stop, reason, reply, new_state}, за исключением того, что ответ не отправляется.

Этот обратчик необязателен. Если он не реализован, сервер потерпит неудачу, если будет выполнен вызов.

handle_cast(request, state)

Спецификации

handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state}
when new_state: term()

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

request — сообщение запроса, отправленное cast/2, а state — текущее состояние GenServer.

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

Возврат {:noreply, new_state, timeout} аналогичен {:noreply, new_state}, но также устанавливает таймаут. Подробнее см. в разделе "Таймауты" в документации модуля.

Возврат {:noreply, new_state, :hibernate} аналогичен {:noreply, new_state}, но процесс переводится в спящее состояние перед продолжением цикла. Подробнее см. handle_call/3.

Возврат {:noreply, new_state, {:continue, continue}} аналогичен {:noreply, new_state}, но handle_continue/2 будет вызвана немедленно после этого со значением continue в качестве первого аргумента.

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

Этот обратчик необязателен. Если он не реализован, сервер потерпит неудачу, если будет выполнен вызов.

handle_continue(continue, state)

Спецификации

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

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

Полезно для выполнения работы после инициализации или для разделения работы в обратном вызове на несколько этапов, обновляя состояние процесса по ходу выполнения.

Значения возврата такие же, как у handle_cast/2.

Этот обратчик необязателен. Если он не реализован, сервер потерпит неудачу, если будет использована инструкция continue.

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

handle_info(msg, state)

Спецификации

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

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

msg — сообщение, а state — текущее состояние GenServer. В случае таймаута сообщение — :timeout.

Значения возврата такие же, как у handle_cast/2.

Этот обратчик необязателен. Если он не реализован, полученное сообщение будет записано в журнал.

END_OF_DOCUMENT_MARKER

init(init_arg)

Характеристики

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

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

init_arg — аргумент (второй аргумент), переданный в start_link/3.

Возврат {:ok, state} заставит start_link/3 вернуть {:ok, pid} и процесс перейти к своей циклической работе.

Возврат {:ok, state, timeout} аналогичен {:ok, state}, за исключением того, что он также устанавливает таймаут. Более подробная информация содержится в разделе «Таймауты» в документации модуля.

Возврат {:ok, state, :hibernate} аналогичен {:ok, state}, за исключением того, что процесс переходит в спящий режим перед входом в цикл. Подробнее о приостановке см. в handle_call/3.

Возврат {:ok, state, {:continue, continue}} аналогичен {:ok, state}, за исключением того, что сразу после входа в цикл вызывается обратный вызов handle_continue/2 со значением continue в качестве первого аргумента.

Возврат :ignore заставит start_link/3 вернуть :ignore и процесс завершится нормально, не войдя в цикл или не вызвав terminate/2. Если используется в дереве надзора, родительский надзиратель не потерпит неудачи при запуске и не попытается немедленно перезапустить GenServer. Остальная часть дерева надзора будет запущена, и поэтому GenServer не должна требоваться другим процессам. Его можно запустить позже с помощью Supervisor.restart_child/2, так как описание дочернего элемента сохраняется в родительском надзирателе. Основные варианты использования:

  • Конфигурация GenServer отключена, но может быть включена позже.
  • Возникла ошибка, и она будет обработана другим механизмом, отличным от Supervisor. Скорее всего, этот подход включает вызов Supervisor.restart_child/2 через некоторое время для попытки перезапуска.

Возврат {:stop, reason} заставит start_link/3 вернуть {:error, reason} и процесс завершится с причиной reason без входа в цикл или вызова terminate/2.

terminate(reason, state)

Характеристики

terminate(reason, state :: term()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()}

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

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

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

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

Если процесс является частью дерева надзора, он получит сигнал завершения при завершении работы дерева. Сигнал завершения основан на стратегии завершения в спецификации дочернего элемента, где это значение может быть:

  • :brutal_kill: процесс GenServer убивается, и terminate/2 не вызывается.

  • значение таймаута, где надзиратель отправит сигнал завершения :shutdown, а у GenServer будет время таймаута для завершения. Если после истечения таймаута процесс все еще жив, он будет немедленно убит.

Более подробное объяснение см. в разделе «Значения завершения (:shutdown)» в модуле Supervisor.

Если GenServer получает сигнал завершения (который не равен :normal) от любого процесса, когда он не обрабатывает завершения, он завершится внезапно с той же причиной и не вызовет terminate/2. Обратите внимание, что процесс НЕ обрабатывает завершения по умолчанию, и сигнал завершения отправляется, когда связанный процесс завершается или его узел отключается.

Поэтому нет гарантии, что terminate/2 будет вызван при завершении GenServer. По этим причинам мы обычно рекомендуем важные правила очистки в отдельных процессах, либо с помощью мониторинга, либо с помощью самих связей. Очистка не требуется, когда GenServer управляет port (например, :gen_tcp.socket) или File.io_device/0, потому что они будут закрыты при получении сигнала завершения GenServer и не нужно закрывать вручную в terminate/2.

Если reason не является :normal, :shutdown, ни {:shutdown, term}, регистрируется ошибка.

Этот обратный вызов необязателен.

© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.9.4/GenServer.html

Spec-Zone.ru

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