Агент
Агенты — это простое абстрагирование состояния.
Часто в Elixir возникает необходимость обмениваться или хранить состояние, к которому необходимо получить доступ из разных процессов или из одного и того же процесса в разное время.
Модуль Agent предоставляет базовую реализацию сервера, которая позволяет получать и обновлять состояние через простой API.
Примеры
Например, в инструменте Mix, который поставляется с Elixir, нам нужно хранить набор всех задач, выполненных заданным проектом. Поскольку этот набор общий, мы можем реализовать его с помощью агента:
defmodule Mix.TasksServer do
use Agent
def start_link(_) do
Agent.start_link(fn -> MapSet.new end, name: __MODULE__)
end
@doc "Checks if the task has already executed"
def executed?(task, project) do
item = {task, project}
Agent.get(__MODULE__, fn set ->
item in set
end)
end
@doc "Marks a task as executed"
def put_task(task, project) do
item = {task, project}
Agent.update(__MODULE__, &MapSet.put(&1, item))
end
@doc "Resets the executed tasks and returns the previous list of tasks"
def take_all() do
Agent.get_and_update(__MODULE__, fn set ->
{Enum.into(set, []), MapSet.new}
end)
end
end Агенты обеспечивают разделение между клиентским и серверным API (аналогично GenServers). В частности, анонимные функции, передаваемые в 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, позволяющую поместить определённый модуль под дерево управления. Сгенерированную child_spec/1 можно настроить с помощью следующих опций:
-
:id— идентификатор спецификации ребёнка, по умолчанию — текущий модуль -
:start— способ запуска дочернего процесса (по умолчанию вызов__MODULE__.start_link/1) -
:restart— когда дочерний процесс должен быть перезапущен, по умолчанию:permanent -
:shutdown— способ завершения дочернего процесса
Например:
use Agent, restart: :transient, shutdown: 10_000
См. документацию Supervisor для получения дополнительной информации.
Регистрация имени
Агент подчиняется тем же правилам регистрации имени, что и 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()
agent() :: pid() | {atom(), node()} | name() Ссылка на агента
name()
name() :: atom() | {:global, term()} | {:via, module(), term()} Имя агента
on_start()
on_start() :: {:ok, pid()} | {:error, {:already_started, pid()} | term()} Значения возвращаемые функциями start*
state()
state() :: term()
Состояние агента
Функции
cast(agent, fun)
cast(agent(), (state() -> state())) :: :ok
Выполняет операцию cast (fire and forget) над состоянием агента.
Функция fun отправляется в agent, которая вызывает функцию, передавая состояние агента. Значение возвращаемое fun становится новым состоянием агента.
Обратите внимание, что cast возвращает :ok немедленно, независимо от того, существует ли agent (или узел, на котором он должен существовать).
cast(agent, module, fun, args)
cast(agent(), module(), atom(), [term()]) :: :ok
Выполняет операцию cast (fire and forget) над состоянием агента.
Аналогично cast/2, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента в заданный список аргументов.
child_spec(arg) (since 1.5.0)
Возвращает спецификацию для запуска агента под управлением диспетчера.
См. 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, которая вызывает функцию, передавая состояние агента. Функция должна вернуть кортеж из двух элементов, первым из которых является значение для возврата (то есть значение «get»), а вторым — новое состояние агента.
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, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется как первый аргумент в заданный список аргументов.
© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.7.4/Agent.html