Spec-Zone.ru › Elixir 1.8

GenServer поведение

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

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

Пример

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

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

defmodule Stack do
  use GenServer

  # Callbacks

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

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

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

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

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

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

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

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

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

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

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

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

defmodule Stack do
  use GenServer

  # Client

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

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

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

  # Server (callbacks)

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

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

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

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

Как настроить надзор

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

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

Supervisor.start_link(children, strategy: :one_for_all)

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

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

Supervisor.start_link(children, strategy: :one_for_all)

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

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

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

Например:

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

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

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

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

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

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

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

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

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

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

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

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

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

Приём «обычных» сообщений

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

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

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

defmodule MyApp.Periodically do
  use GenServer

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

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

    {:ok, state}
  end

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

    # Reschedule once more
    schedule_work()

    {:noreply, state}
  end

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

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

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

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

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

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

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

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

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

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

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

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

Отладка с помощью модуля :sys

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

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

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

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

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

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

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

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

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

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

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

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

  • GenServer — Руководство Elixir по началу работы
  • :gen_server документация по модулю
  • Gen_server Behaviour — Принципы проектирования OTP
  • Клиенты и серверы — Учитесь Erlang

Резюме

Типы

debug()

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

from()

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

name()

Имя GenServer

on_start()

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

option()

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

options()

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

server()

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

Функции

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

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

call(server, request, timeout \\ 5000)

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

cast(server, request)

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

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

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

reply(client, reply)

Отвечает клиенту.

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

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

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

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

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

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

whereis(server)

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

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

code_change(old_vsn, state, extra)

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

format_status(reason, pdict_and_state)

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

handle_call(request, from, state)

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

handle_cast(request, state)

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

handle_continue(continue, state)

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

handle_info(msg, state)

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

init(init_arg)

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

terminate(reason, state)

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

Типы

debug()

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

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

from()

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

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

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

name()

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

Имя GenServer

on_start()

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

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

option()

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

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

options()

options() :: [option()]

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

server()

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

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

Функции

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

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

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

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

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

call(server, request, timeout \\ 5000)

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

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

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

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

Таймауты

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

cast(server, request)

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

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

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

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

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

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

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

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

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

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

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

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

Примеры

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

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

reply(client, reply)

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

Отвечает клиенту.

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

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

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

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

Примеры

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

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

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

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

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

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

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

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

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

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

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

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

Параметры

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

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

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

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

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

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

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

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

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

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

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

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

Функция сохраняет семантику OTP относительно сообщения об ошибках. Если причина отличается от :normal, :shutdown или {:shutdown, _}, выводится сообщение об ошибке.

whereis(server)

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

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

Примеры

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

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

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

code_change(old_vsn, state, extra)

(необязательно)
code_change(old_vsn, state :: term(), extra :: term()) ::
  {:ok, new_state :: term()} | {:error, reason :: term()} | {:down, term()}
when old_vsn: term()

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

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

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

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

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

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

format_status(reason, pdict_and_state)

(необязательно)
format_status(reason, pdict_and_state :: list()) :: term()
when reason: :normal | :terminate

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

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

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

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

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

handle_call(request, from, state)

(необязательно)
handle_call(request :: term(), from(), state :: term()) ::
  {:reply, reply, new_state}
  | {:reply, reply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason, reply, new_state}
  | {:stop, reason, new_state}
when reply: term(), new_state: term(), reason: term()

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

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

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

Возврат {:reply, reply, new_state, timeout} аналогичен {:reply, reply, new_state} за исключением того, что handle_info(:timeout, new_state) будет вызван через timeout миллисекунд, если сообщения не получены.

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

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

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

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

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

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

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

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

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

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

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

handle_cast(request, state)

(необязательно)
handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state}
when new_state: term()

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

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

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

Возврат {:noreply, new_state, timeout} аналогичен {:noreply, new_state} за исключением того, что handle_info(:timeout, new_state) будет вызван через timeout миллисекунд, если сообщения не получены.

Возврат {:noreply, new_state, :hibernate} аналогичен {:noreply, new_state} за исключением того, что процесс переходит в состояние ожидания (гибернацию) перед продолжением цикла. Дополнительную информацию см. в handle_call/3.

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

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

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

handle_continue(continue, state)

(необязательно)
handle_continue(continue :: term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state}
when new_state: term()

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

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

Возвращаемые значения такие же, как у handle_cast/2.

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

Этот обратный вызов поддерживается только в Erlang/OTP 21 и более поздних версиях.

handle_info(msg, state)

(необязательно)
handle_info(msg :: :timeout | term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state}
when new_state: term()

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

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

Возвращаемые значения такие же, как у handle_cast/2.

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

init(init_arg)

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

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

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

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

Возврат {:ok, state, timeout} аналогичен {:ok, state} за исключением того, что handle_info(:timeout, state) будет вызван через timeout миллисекунд, если сообщение не получено в течение таймаута.

Возврат {:ok, state, :hibernate} аналогичен {:ok, state} за исключением того, что процесс переходит в состояние ожидания (гибернацию) перед входом в цикл. Дополнительную информацию о гибернации см. в handle_call/3.

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

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

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

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

terminate(reason, state)

(необязательно)
terminate(reason, state :: term()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()}

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

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

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

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

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

  • :brutal_kill: GenServer уничтожается, поэтому terminate/2 не вызывается.

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

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

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

END_OF_DOCUMENT_MARKER

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

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

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

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

Spec-Zone.ru

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