Spec-Zone.ru › Elixir 1.17

Исходный код 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 — абстрагировать цикл «receive» для разработчиков, автоматически обрабатывая системные сообщения, поддерживая изменения кода, синхронные вызовы и многое другое. Поэтому вы никогда не должны вызывать свой собственный «receive» внутри обратных вызовов 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 :: 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_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(), 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, _}, регистрируется отчёт об ошибке.

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.34.1) для язык программирования Elixir

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

Spec-Zone.ru

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