Spec-Zone.ru › Elixir 1.15

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)

Выполняет операцию отправки (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()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
END_OF_DOCUMENT_MARKER

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.15.4/Agent.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API