Агент
Агенты — это простое абстрагирование состояния.
Часто в 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) 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— идентификатор спецификации ребёнка, по умолчанию текущий модуль -
:start— как запустить дочерний процесс (по умолчанию вызов__MODULE__.start_link/1) -
: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)
Выполняет операцию броска (fire and forget) над состоянием агента.
- cast(agent, module, fun, args)
Выполняет операцию броска (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
Выполняет операцию броска (fire and forget) над состоянием агента.
Функция fun отправляется в agent, который вызывает функцию, передавая состояние агента. Возвращаемое значение fun становится новым состоянием агента.
Обратите внимание, что cast возвращает :ok немедленно, независимо от того, существует ли agent (или узел, на котором он должен существовать).
cast(agent, module, fun, args)
cast(agent(), module(), atom(), [term()]) :: :ok
Выполняет операцию броска (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, которая вызывает функцию, передавая состояние агента. Функция должна возвращать кортеж из двух элементов: первый — значение, которое нужно вернуть («полученное» значение), а второй — новое состояние агента.
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, агенту разрешается потратить не более заданного количества миллисекунд на инициализацию, иначе он будет завершён, и функция start вернёт {: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.8.2/Agent.html