Agent
Agents — это простое абстрагирование состояния.
Часто в 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 (отправка и забыть) на состоянии агента.
- 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()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, которая вызывает функцию, передавая состояние агента. Функция должна вернуть кортеж с двумя элементами, первый из которых — значение для возврата (значение «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)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.14.1/Agent.html