Spec-Zone.ru › Elixir 1.13

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. Существует 8 возможных вызовов для реализации при использовании 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, который использует эти функции для хранения списка имен процессов и их связанных идентификаторов процесса, доступных глобально для сети узлов 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
    # We schedule the work to happen in 2 hours (written in milliseconds).
    # Alternatively, one might write :timer.hours(2)
    Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
  end
end

Таймауты

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

Значение timeout() используется следующим образом:

  • Если процесс уже имеет ожидающее сообщение, когда возвращается значение timeout(), таймаут игнорируется, и ожидающее сообщение обрабатывается как обычно. Это означает, что даже таймаут в 0 миллисекунд не гарантирован (если вы хотите выполнить другое действие немедленно и безусловно, используйте инструкцию :continue вместо этого).

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

  • В противном случае, когда указанное количество миллисекунд истекает без прихода сообщения, вызывается handle_info/2 с :timeout в качестве первого аргумента.

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

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

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

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

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

def subtract(a, b) do
  GenServer.call(__MODULE__, {:subtract, 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()

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

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

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)

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

Функции

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 в противном случае.

Типы

debug()Source

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

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

from()Source

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

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

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

name()Source

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

Имя GenServer

on_start()Source

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

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

option()Source

@type option() ::
  {:debug, debug()}
  | {:name, name()}
  | {:timeout, timeout()}
  | {:spawn_opt, [Process.spawn_opt()]}
  | {:hibernate_after, timeout()}

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

options()Source

@type options() :: [option()]

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

server()Source

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

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

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

Обработчики

code_change(old_vsn, state, extra)Source

@callback 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)Source

@callback 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)Source

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

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

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

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

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

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

@callback 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.

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

handle_continue(continue, state)Source

@callback 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.

handle_info(msg, state)Source

@callback 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)Source

@callback 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)Source

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

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

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

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

  • возвращает кортеж :stop
  • вызывает исключение (через Kernel.raise/2) или завершает работу (через Kernel.exit/1)
  • возвращает недопустимое значение

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

  • :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}, выводится ошибка.

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

Функции

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

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

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

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

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

call(server, request, timeout \\ 5000)Source

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

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

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

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

Таймауты

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

cast(server, request)Source

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

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

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

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

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

@spec 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)Source

@spec 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 \\ [])Source

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

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

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

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

@spec 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)Source

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

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

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

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

END_OF_DOCUMENT_MARKER

whereis(сервер)Исходный код

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

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

Точнее, nil возвращается всякий раз, когда pid или {name, node} не могут быть возвращены. Обратите внимание, что нет гарантии, что возвращенный pid или {name, node} жив, так как процесс может завершиться сразу после поиска.

Примеры

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

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

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

Spec-Zone.ru

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