Spec-Zone.ru › Elixir 1.10

Агент

Агенты — это простое абстрагирование состояния.

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

Модуль Agent предоставляет базовую реализацию сервера, которая позволяет получать и обновлять состояние через простой API.

Примеры

Например, следующий агент реализует счётчик:

defmodule Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end

Использование было бы следующим:

Counter.start_link(0)
#=> {:ok, #PID<0.123.0>}

Counter.value()
#=> 0

Counter.increment()
#=> :ok

Counter.increment()
#=> :ok

Counter.value()
#=> 2

Благодаря процессу-серверу агента, счётчик можно безопасно увеличивать параллельно.

Агенты обеспечивают разделение между клиентским и серверным API (аналогично GenServer). В частности, функции, переданные в качестве аргументов вызовам функций Agent, вызываются внутри агента (сервера). Это различие важно, так как вы можете захотеть избежать дорогостоящих операций внутри агента, так как они фактически заблокируют агент до выполнения запроса.

Рассмотрим эти два примера:

# Compute in the agent/server
def get_something(agent) do
  Agent.get(agent, fn state -> do_something_expensive(state) end)
end

# Compute in the agent/client
def get_something(agent) do
  Agent.get(agent, & &1) |> do_something_expensive()
end

Первая функция блокирует агент. Вторая функция копирует всё состояние в клиента и затем выполняет операцию на клиенте. Один из аспектов, который следует учитывать, это достаточно ли велики данные, чтобы потребовать обработки на сервере, по крайней мере, изначально, или достаточно малы, чтобы их можно было передать клиенту без проблем. Другой фактор — требуется ли обработка данных атомарно: получение состояния и вызов do_something_expensive(state) вне агента означает, что состояние агента может быть обновлено в то же время. Это особенно важно в случае обновлений, так как вычисление нового состояния на клиенте, а не на сервере, может привести к гонкам, если несколько клиентов пытаются обновить одно и то же состояние до разных значений.

Как выполнять надзор

Агент обычно запускается в дереве надзора. Когда мы вызываем use Agent, он автоматически определяет функцию child_spec/1, которая позволяет нам запускать агент непосредственно под надзором. Чтобы запустить агент под надзором с начальным счётчиком 0, можно сделать так:

children = [
  {Counter, 0}
]

Supervisor.start_link(children, strategy: :one_for_all)

Хотя можно также просто передать Counter в качестве дочернего элемента надзору, например:

children = [
  Counter # Same as {Counter, []}
]

Supervisor.start_link(children, strategy: :one_for_all)

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

def start_link(opts) do
  {initial_value, opts} = Keyword.pop(opts, :initial_value, 0)
  Agent.start_link(fn -> initial_value end, opts)
end

и тогда вы можете использовать Counter, {Counter, name: :my_counter} или даже {Counter, initial_value: 0, name: :my_counter} в качестве спецификации дочернего элемента.

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

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

Например:

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

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

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

Агент подчиняется тем же правилам регистрации имён, что и GenServer. Подробнее об этом читайте в документации GenServer.

Несколько слов о распределённых агентах

Важно учитывать ограничения распределённых агентов. Агенты предоставляют два API: один, который работает с анонимными функциями, и другой, который ожидает явного модуля, функции и аргументов.

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

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

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

Горячая замена кода

Код агента можно заменить «на лету», просто передав кортеж модуля, функции и аргументов в инструкцию обновления. Например, представьте, что у вас есть агент с именем :sample и вы хотите преобразовать его внутреннее состояние из списка ключевых слов в карту. Это можно сделать с помощью следующей инструкции:

{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}

Состояние агента будет добавлено в данный список аргументов ([%{}]) в качестве первого аргумента.

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

Типы

agent()

Ссылка на агента

name()

Имя агента

on_start()

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

state()

Состояние агента

Функции

cast(agent, fun)

Выполняет операцию cast (отправить и забыть) над состоянием агента.

cast(agent, module, fun, args)

Выполняет операцию cast (отправить и забыть) над состоянием агента.

child_spec(arg)

Возвращает спецификацию запуска агента под надзором.

get(agent, fun, timeout \\ 5000)

Получает значение агента с помощью заданной анонимной функции.

get(agent, module, fun, args, timeout \\ 5000)

Получает значение агента с помощью заданной функции.

get_and_update(agent, fun, timeout \\ 5000)

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

get_and_update(agent, module, fun, args, timeout \\ 5000)

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

start(fun, options \\ [])

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

start(module, fun, args, options \\ [])

Запускает агент без ссылок с заданным модулем, функцией и аргументами.

start_link(fun, options \\ [])

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

start_link(module, fun, args, options \\ [])

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

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

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

update(agent, fun, timeout \\ 5000)

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

update(agent, module, fun, args, timeout \\ 5000)

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

Типы

agent()

Specs

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

Ссылка на агента

name()

Specs

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

Имя агента

on_start()

Specs

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

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

state()

Specs

state() :: term()

Состояние агента

Функции

cast(agent, fun)

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

cast(agent(), (state() -> state())) :: :ok

Выполняет операцию преобразования (fire and forget) состояния агента.

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

Обратите внимание, что cast возвращает :ok немедленно, независимо от того, существует ли agent (или узел, на котором он должен существовать).

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.cast(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43

cast(agent, module, fun, args)

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

cast(agent(), module(), atom(), [term()]) :: :ok

Выполняет операцию преобразования (fire and forget) состояния агента.

Аналогично cast/2, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента к предоставленному списку аргументов.

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.cast(pid, Kernel, :+, [12])
:ok
iex> Agent.get(pid, fn state -> state end)
54

child_spec(arg)

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

См. раздел «Спецификация дочернего процесса» в модуле Supervisor для получения более подробной информации.

get(agent, fun, timeout \\ 5000)

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

get(agent(), (state() -> a), timeout()) :: a when a: var

Получает значение агента через заданную анонимную функцию.

Функция fun отправляется agent, которая вызывает функцию, передавая состояние агента. Результат вызова функции возвращается из этой функции.

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

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42

get(agent, module, fun, args, timeout \\ 5000)

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

get(agent(), module(), atom(), [term()], timeout()) :: any()

Получает значение агента через заданную функцию.

Аналогично get/3, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента к предоставленному списку аргументов.

get_and_update(agent, fun, timeout \\ 5000)

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

get_and_update(agent(), (state() -> {a, state()}), timeout()) :: a when a: var

Получает и обновляет состояние агента в одной операции через заданную анонимную функцию.

Функция fun отправляется agent, которая вызывает функцию, передавая состояние агента. Функция должна возвращать кортеж с двумя элементами: первым — значение для возврата (т.е. значение «получить»), а вторым — новое состояние агента.

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

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get_and_update(pid, fn state -> {state, state + 1} end)
42
iex> Agent.get(pid, fn state -> state end)
43

get_and_update(agent, module, fun, args, timeout \\ 5000)

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

get_and_update(agent(), module(), atom(), [term()], timeout()) :: any()

Получает и обновляет состояние агента в одной операции через заданную функцию.

Аналогично get_and_update/3, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента к предоставленному списку аргументов.

start(fun, options \\ [])

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

start((() -> term()), GenServer.options()) :: on_start()

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

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

Примеры

iex> {:ok, pid} = Agent.start(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42

start(module, fun, args, options \\ [])

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

start(module(), atom(), [any()], GenServer.options()) :: on_start()

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

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

start_link(fun, options \\ [])

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

start_link((() -> term()), GenServer.options()) :: on_start()

Запускает агента, связанного с текущим процессом, с помощью заданной функции.

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

После запуска агента, заданная функция fun вызывается в процессе сервера и должна вернуть начальное состояние агента. Обратите внимание, что start_link/2 не возвращает значение, пока заданная функция не вернёт результат.

Параметры

Параметр :name используется для регистрации, как описано в документации модуля.

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

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

Если параметр :spawn_opt присутствует, его значение будет передано в качестве параметров в базовый процесс, как в Process.spawn/4.

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

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

Если заданная функция обратного вызова завершилась ошибкой, функция возвращает {:error, reason}.

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42

iex> {:error, {exception, _stacktrace}} = Agent.start(fn -> raise "oops" end)
iex> exception
%RuntimeError{message: "oops"}

start_link(module, fun, args, options \\ [])

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

start_link(module(), atom(), [any()], GenServer.options()) :: on_start()

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

Аналогично start_link/2, но вместо анонимной функции ожидаются модуль, функция и аргументы; fun в module будет вызван с предоставленными аргументами args для инициализации состояния.

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

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

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

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

Возвращает :ok если агент завершается с указанным причиной. Если агент завершается с другой причиной, вызов завершается ошибкой.

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

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.stop(pid)
:ok

update(agent, fun, timeout \\ 5000)

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

update(agent(), (state() -> state()), timeout()) :: :ok

Обновляет состояние агента через заданную анонимную функцию.

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

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

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

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43

update(agent, module, fun, args, timeout \\ 5000)

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

update(agent(), module(), atom(), [term()], timeout()) :: :ok

Обновляет состояние агента через заданную функцию.

Аналогично update/3, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента к предоставленному списку аргументов.

Примеры

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, Kernel, :+, [12])
:ok
iex> Agent.get(pid, fn state -> state end)
54

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

Spec-Zone.ru

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