Agents are a simple abstraction around state.
Often in Elixir there is a need to share or store state that must be accessed from different processes or by the same process at different points in time.
The Agent module provides a basic server implementation that allows state to be retrieved and updated via a simple API.
Examples
For example, in the Mix tool that ships with Elixir, we need to keep a set of all tasks executed by a given project. Since this set is shared, we can implement it with an Agent:
defmodule Mix.TasksServer do
def start_link do
Agent.start_link(fn -> HashSet.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__, &Set.put(&1, item))
end
end
Note that agents still provide a segregation between the client and server APIs, as seen in GenServers. In particular, all code inside the function passed to the agent is executed by the agent. This distinction is important because you may want to avoid expensive operations inside the agent, as it will effectively block the agent until the request is fulfilled.
Consider these two examples:
# 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
The first one blocks the agent while the second one copies all the state to the client and executes the operation in the client. The trade-off here is exactly if the data is small enough to be sent to the client cheaply or large enough to require processing on the server (or at least some initial processing).
Name Registration
An Agent is bound to the same name registration rules as GenServers.
Read more about it in the GenServer
docs.
A word on distributed agents
It is important to consider the limitations of distributed agents. Agents provides two APIs, one that works with anonymous functions and another that expects explicit module, function and arguments.
In a distributed setup with multiple nodes, the API that accepts anonymous functions only works if the caller (client) and the agent have the same version of the caller module.
Keep in mind this issue also shows up when performing "rolling upgrades" with agents. By rolling upgrades we mean the following situation: you wish to deploy a new version of your software by shutting down some of your nodes and replacing them with nodes running a new version of the software. In this setup, part of your environment will have one version of a given module and the other part another version (the newer one) of the same module.
The best solution is to simply use the explicit module, function and arguments APIs when working with distributed agents.
Hot code swapping
An agent can have its code hot swapped live by simply passing a module,
function and args tuple to the update instruction. For example, imagine
you have an agent named :sample
and you want to convert its inner state
from some dict structure to a map. It can be done with the following
instruction:
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
The agent's state will be added to the given list as the first argument.