Spec-Zone.ru › Elixir 1.18

Исходный код GenServer поведение

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

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

graph BT
    C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
    A & B & C -->|request| GenServer
    GenServer -.->|reply| A & B & C

Пример

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

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

init/1 преобразует наш начальный аргумент в начальное состояние для GenServer. handle_call/3 срабатывает, когда сервер получает синхронное pop сообщение, извлекая элемент из стека и возвращая его пользователю. handle_cast/2 сработает, когда сервер получит асинхронное push сообщение, помещая элемент в стек:

defmodule Stack do
  use GenServer

  # Callbacks

  @impl true
  def init(elements) do
    initial_state = String.split(elements, ",", trim: true)
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:pop, _from, state) do
    [to_caller | new_state] = state
    {:reply, to_caller, new_state}
  end

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

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

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

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

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

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

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

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

use GenServer

При use GenServer, модуль GenServer установит @behaviour GenServer и определит функцию child_spec/1, чтобы ваш модуль мог использоваться как дочерний в дереве надзора.

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

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

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

defmodule Stack do
  use GenServer

  # Client

  def start_link(default) when is_binary(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(elements) do
    initial_state = String.split(elements, ",", trim: true)
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:pop, _from, state) do
    [to_caller | new_state] = state
    {:reply, to_caller, new_state}
  end

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

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

На следующей схеме показаны взаимодействия между клиентом и сервером. Клиент и сервер являются процессами, а общение происходит через сообщения (сплошная линия). Взаимодействие Сервер <-> Модуль происходит, когда процесс GenServer вызывает ваш код (пунктирные линии):

sequenceDiagram
    participant C as Client (Process)
    participant S as Server (Process)
    participant M as Module (Code)

    note right of C: Typically started by a supervisor
    C->>+S: GenServer.start_link(module, arg, options)
    S-->>+M: init(arg)
    M-->>-S: {:ok, state} | :ignore | {:error, reason}
    S->>-C: {:ok, pid} | :ignore | {:error, reason}

    note right of C: call is synchronous
    C->>+S: GenServer.call(pid, message)
    S-->>+M: handle_call(message, from, state)
    M-->>-S: {:reply, reply, state} | {:stop, reason, reply, state}
    S->>-C: reply

    note right of C: cast is asynchronous
    C-)S: GenServer.cast(pid, message)
    S-->>+M: handle_cast(message, state)
    M-->>-S: {:noreply, state} | {:stop, reason, state}

    note right of C: send is asynchronous
    C-)S: Kernel.send(pid, message)
    S-->>+M: handle_info(message, state)
    M-->>-S: {:noreply, state} | {:stop, reason, state}

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

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

children = [
  {Stack, "hello,world"}
]

Supervisor.start_link(children, strategy: :one_for_all)

Обратите внимание, что указание модуля MyServer эквивалентно указанию кортежа {MyServer, []}.

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

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

Например:

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

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

При остановке GenServer, например, возвращая кортеж {:stop, reason, new_state} из обратного вызова, причина выхода используется надзирателем для определения необходимости перезапуска GenServer. См. раздел «Причины выхода и перезапуски» в модуле Supervisor.

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

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

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

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

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

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

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

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

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

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

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

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

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

Помимо синхронной и асинхронной связи, обеспечиваемой call/3 и cast/2, "обычные" сообщения, отправленные такими функциями, как 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/1.
  • :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]}]
   ]
 ]}

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

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

  • GenServer - Руководство по началу работы с Elixir
  • :gen_server документация модуля
  • gen_server Behaviour - Принципы проектирования OTP
  • Клиенты и серверы - Learn You Some Erlang for Great Good!

Краткое описание

Типы

debug()

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

from()

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

name()

Имя GenServer.

on_start()

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

option()

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

options()

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

server()

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

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

code_change(old_vsn, state, extra)

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

format_status(status)

Эта функция вызывается процессом GenServer в следующих ситуациях

format_status(reason, pdict_and_state) устаревший
handle_call(request, from, state)

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

handle_cast(request, state)

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

handle_continue(continue_arg, state)

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

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, либо значение, представляющее зарегистрированное имя. Дополнительную информацию см. в разделе "Регистрация имён" данного документа.

END_OF_DOCUMENT_MARKER

Обработчики

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) при обновлении. При понижении версии предыдущая версия упаковывается в пару из двух элементов, где первый элемент :down. state — текущее состояние GenServer, а extra — любые дополнительные данные, необходимые для изменения состояния.

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

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

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

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

format_status(status)Source

@callback format_status(status :: :gen_server.format_status()) ::
  new_status :: :gen_server.format_status()

Эта функция вызывается процессом GenServer в следующих ситуациях:

  • :sys.get_status/1,2 вызывается для получения статуса GenServer.
  • Процесс GenServer завершается аномально и регистрирует ошибку.

Этот обработчик используется для ограничения статуса процесса, возвращаемого функцией :sys.get_status/1,2 или отправляемого в логирование.

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

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

Пример

@impl GenServer
def format_status(status) do
  Map.new(status, fn
    {:state, state} -> {:state, Map.delete(state, :private_key)}
    {:message, {:password, _}} -> {:message, {:password, "redacted"}}
    key_value -> key_value
  end)
end

format_status(reason, pdict_and_state)Source

Этот обработчик устарел. Используйте обработчик format_status/1 вместо него.
@callback format_status(reason, pdict_and_state :: list()) :: term()
when reason: :normal | :terminate

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, continue_arg :: term()}}
  | {:noreply, new_state}
  | {:noreply, new_state,
     timeout() | :hibernate | {:continue, continue_arg :: 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_arg}} аналогичен {:reply, reply, new_state} за исключением того, что handle_continue/2 будет вызван немедленно после с continue_arg в качестве первого аргумента и state в качестве второго.

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

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

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

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

Возврат {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg}} аналогичен {: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, continue_arg :: 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_arg}} аналогичен {:noreply, new_state} за исключением того, что handle_continue/2 будет вызван немедленно после с continue_arg в качестве первого аргумента и state в качестве второго.

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

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

handle_continue(continue_arg, state)Source

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

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

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

Возвращаемые значения аналогичны handle_cast/2.

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

handle_info(msg, state)Source

@callback handle_info(msg :: :timeout | term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state,
     timeout() | :hibernate | {:continue, continue_arg :: 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, continue_arg :: term()}}
  | :ignore
  | {:stop, reason :: term()}
when state: term()

Вызывается при запуске сервера. 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_arg}} аналогичен {:ok, state}, за исключением того, что сразу после входа в цикл вызывается обратный вызов handle_continue/2 с continue_arg в качестве первого аргумента и state в качестве второго.

Возврат :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. Однако не гарантируется, что terminate/2 вызывается при выходе GenServer. Поэтому важную очистку следует выполнять с помощью связей и/или мониторов процессов. Мониторинговый процесс получит тот же код завершения reason, что и был бы передан в terminate/2.

terminate/2 вызывается, если:

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

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

    • возвращает кортеж :stop

    • вызывает исключение (через raise/2) или завершается (через exit/1)

    • возвращает недопустимое значение

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

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

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

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

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

terminate/2 вызывается только после того, как GenServer завершит обработку всех сообщений, которые поступили в его почтовый ящик до сигнала завершения. Если он получает сигнал :kill до того, как завершит обработку этих сообщений, terminate/2 не будет вызван. Если terminate/2 вызывается, любые сообщения, полученные после сигнала завершения, останутся в почтовом ящике.

Очистка не требуется, когда 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(), term(), options()) :: on_start()

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

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

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

@spec start_link(module(), term(), 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, _}, регистрируется сообщение об ошибке.

whereis(server)Source

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

Скачать версию ePub

Создано с помощью ExDoc (v0.36.1) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/GenServer.html

Spec-Zone.ru

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