Исходный код Agent
Агенты — это простое абстрактное представление состояния.
Часто в 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
Благодаря процессу-серверу агента, счётчик может быть безопасно инкрементирован параллельно.
use Agent
Когда вы use Agent, модуль Agent определит функцию child_spec/1, чтобы ваш модуль можно было использовать в качестве дочернего элемента в дереве надзора.
Агенты обеспечивают разделение между клиентским и серверным 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) вне агента означает, что состояние агента может быть обновлено тем временем. Это особенно важно в случае обновлений, поскольку вычисление нового состояния на клиенте, а не на сервере, может привести к гонкам, если несколько клиентов пытаются обновить одно и то же состояние до разных значений.
Как настроить надзор
Агент Agent обычно запускается в дереве надзора. При вызове 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 отправляется агенту, который вызывает функцию, передавая состояние агента. Возвращаемое значение 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 отправляется агенту, который вызывает функцию, передавая состояние агента. Результат вызова функции возвращается из этой функции.
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()) :: term()
Получает значение агента с помощью заданной функции.
Аналогично get/3, но вместо анонимной функции ожидаются модуль, функция и аргументы. Состояние добавляется в качестве первого аргумента к списку аргументов.
get_and_update(agent, fun, timeout \\ 5000)Source
@spec get_and_update(agent(), (state() -> {a, state()}), timeout()) :: a when a: var Получает и обновляет состояние агента в одной операции с помощью заданной анонимной функции.
Функция fun отправляется агенту, который вызывает функцию, передавая состояние агента. Функция должна вернуть кортеж с двумя элементами: первый — значение для возврата (значение "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()) :: term()
Получает и обновляет состояние агента в одной операции с помощью заданной функции.
Аналогично 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(), [term()], 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(), [term()], 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 отправляется агенту, который вызывает функцию, передавая состояние агента. Возвращаемое значение 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-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/Agent.html