Агент
Агенты — это простое абстрактное представление состояния.
Часто в 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.
Регистрация имён
Агент подчиняется тем же правилам регистрации имён, что и GenServers. Подробнее об этом см. в документации GenServer.
Несколько слов о распределённых агентах
Важно учитывать ограничения распределённых агентов. Агенты предоставляют два API: один, работающий с анонимными функциями, и другой, ожидающий явного модуля, функции и аргументов.
В распределённой настройке с несколькими узлами API, принимающий анонимные функции, работает только в том случае, если у вызывающей стороны (клиента) и агента одинаковая версия модуля вызывающей стороны.
Помните, что эта проблема также возникает при выполнении «постепенных обновлений» с агентами. Под постепенными обновлениями мы подразумеваем следующую ситуацию: вы хотите развернуть новую версию своего программного обеспечения, останавливая некоторые узлы и заменяя их узлами, работающими с новой версией программного обеспечения. В этой настройке часть вашей среды будет иметь одну версию данного модуля, а другая часть — другую (новую) версию того же модуля.
Лучшее решение — использовать API с явным модулем, функцией и аргументами при работе с распределёнными агентами.
Горячая замена кода
Код агента можно заменить на лету, просто передав кортеж модуля, функции и аргументов в инструкцию обновления. Например, представьте, что у вас есть агент с именем :sample и вы хотите преобразовать его внутреннее состояние из списка ключевых слов в карту. Это можно сделать с помощью следующей инструкции:
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
Состояние агента будет добавлено в заданный список аргументов ([%{}]) в качестве первого аргумента.
Сводка
Типы
- agent()
Ссылка на агент
- name()
Имя агента
- on_start()
Значения возвращаемые функциями
start*- state()
Состояние агента
Функции
- cast(agent, fun)
Выполняет операцию cast (fire and forget) над состоянием агента.
- cast(agent, module, fun, args)
Выполняет операцию cast (fire and forget) над состоянием агента.
- 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()Source
@type agent() :: pid() | {atom(), node()} | name() Ссылка на агента
name()Source
@type name() :: atom() | {:global, term()} | {:via, module(), term()} Имя агента
on_start()Source
@type on_start() :: {:ok, pid()} | {:error, {:already_started, pid()} | term()} Возвращаемые значения функций start*
state()Source
@type state() :: term()
Состояние агента
Функции
cast(agent, fun)Source
@spec cast(agent(), (state() -> state())) :: :ok
Выполняет операцию cast (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)Source
@spec cast(agent(), module(), atom(), [term()]) :: :ok
Выполняет операцию cast (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)Source
Возвращает спецификацию для запуска агента под управлением надзирателя.
См. раздел «Спецификация дочернего процесса» в модуле Supervisor для получения более подробной информации.
get(agent, fun, timeout \\ 5000)Source
@spec 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)Source
@spec get(agent(), module(), atom(), [term()], timeout()) :: any()
Получает значение агента через заданную функцию.
Аналогично get/3, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента в переданный список аргументов.
get_and_update(agent, fun, timeout \\ 5000)Source
@spec 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)Source
@spec get_and_update(agent(), module(), atom(), [term()], timeout()) :: any()
Получает и обновляет состояние агента в одной операции через заданную функцию.
Аналогично get_and_update/3, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента в переданный список аргументов.
start(fun, options \\ [])Source
@spec 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 \\ [])Source
@spec start(module(), atom(), [any()], GenServer.options()) :: on_start()
Запускает агента без связей с заданным модулем, функцией и аргументами.
См. start_link/4 для получения дополнительной информации.
start_link(fun, options \\ [])Source
@spec 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 \\ [])Source
@spec start_link(module(), atom(), [any()], GenServer.options()) :: on_start()
Запускает агента, связанного с текущим процессом.
Аналогично start_link/2, но вместо анонимной функции ожидаются модуль, функция и аргументы; fun в module будет вызвана с заданными аргументами args для инициализации состояния.
stop(agent, reason \\ :normal, timeout \\ :infinity)Source
@spec 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)Source
@spec 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)Source
@spec 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.13.4/Agent.html