Powered by AppSignal & Oban Pro

Agents

chapters/ch_8.0_agents.livemd

Agents

Navigation

Home Supervised tasks Gotchas

Introduction

When it comes to storing state in Elixir, we have several options at our disposal. We can utilize the process dictionary for local access, employ GenServers, utilize ETS tables, and more. However, one straightforward and convenient approach is to use Agents. Agents provide a simple abstraction for storing state in a process and offer a straightforward API for accessing and updating the stored state. Internally, Agents are implemented as GenServer processes.

Usage

Using an agent is incredibly straightforward. Let’s take a look at a simple example:

{:ok, my_counter_pid} = Agent.start_link(fn -> 0 end)

# Retrieve the stored state
Agent.get(my_counter_pid, fn state -> state end)
|> IO.inspect(label: "GET")

# Perform a GenServer.cast/2 operation on the agent state
# The caller process will not wait for the operation to complete
Agent.cast(my_counter_pid, fn state -> state + 1 end)
|> IO.inspect(label: "CAST")

# Update the Agent state
# Implemented as a GenServer.call/2 the caller process will wait until the agent process replies
Agent.update(my_counter_pid, fn state -> state + 1 end)
|> IO.inspect(label: "UPDATE")

# Get and update the state in a single call
# Implemented as a GenServer.call/2
Agent.get_and_update(my_counter_pid, fn state -> {state, state + 1} end)
|> IO.inspect(label: "get_and_update")

# Retrieve the stored state
Agent.get(my_counter_pid, fn state -> state end)
|> IO.inspect(label: "GET")

# Stop the agent process
Agent.stop(my_counter_pid)

A agent can also be implemented as a module.

defmodule FibonacciStore do
  use Agent

  @doc "Starts the FibonacciStore agent"
  def start_link(_) do
    Agent.start_link(fn -> [0, 1] end, name: __MODULE__)
  end

  @doc "Gets the next number in the Fibonacci series"
  def next_fibonacci() do
    Agent.get_and_update(__MODULE__, fn [num1, num2] ->
      next_fibonacci = num1 + num2
      {next_fibonacci, [num2, next_fibonacci]}
    end)
  end

  @doc "Gets the last generated number in the Fibonacci series"
  def current_fibonacci() do
    Agent.get(__MODULE__, fn [_num1, num2] -> num2 end)
  end
end

Now lets take our agent server for a spin.

# Stop the agent process if already running
if Process.whereis(FibonacciStore), do: Agent.stop(FibonacciStore)

# Start the agent process
FibonacciStore.start_link(:noop)

# Print the current Fibonacci number in the Agent state
FibonacciStore.current_fibonacci() |> IO.inspect(label: "Current Fibonacci")

# Print the next 10 Fibonacci numbers
Enum.each(1..10, fn _ -> IO.puts(FibonacciStore.next_fibonacci()) end)

# Print the current Fibonacci number in the Agent state
FibonacciStore.current_fibonacci() |> IO.inspect(label: "Current Fibonacci")

# Stop the agent process
FibonacciStore
|> Process.whereis()
|> Agent.stop()

Supervising Agents

Typically, agents are included in a supervision tree, much like GenServers. When we use use Agent in our module, it automatically creates a child_spec/1 function, which enables us to start the agent directly under a supervisor.

The process of adding an agent to a supervision tree closely resembles that of a GenServer. To illustrate, let’s consider starting our FibonacciStore agent under a supervisor:

# Same as {FibonacciStore, []}
children = [FibonacciStore]
Supervisor.start_link(children, strategy: :one_for_all)

# Generate 5 numbers in the Fibonacci series
Enum.each(1..5, fn _ -> FibonacciStore.next_fibonacci() end)

FibonacciStore.current_fibonacci() |> IO.inspect(label: "State before restart")

# Simulate termination of the agent server to check if the supervisor restarts it
FibonacciStore
|> Process.whereis()
|> Process.exit(:boom)

# Wait for the supervisor to restart the agent process
:timer.sleep(200)

# Notice that the agent process was restarted and its state is now set to its initial state
FibonacciStore.current_fibonacci() |> IO.inspect(label: "State after restart")

In addition, similar to GenServers, the use Agent macro also accepts a list of options to customize the child specification and determine its behavior under a supervisor. By providing these options, we can tailor how the agent operates within the supervision tree. The following options can be passed:

  • :id - Specifies the identifier for the child specification. By default, it is set to the current module’s name.
  • :restart - Determines the restart strategy for the child. The default value is :permanent, which restarts the child process regardless of whether it crashes or is gracefully terminated.

Here’s an example of using the use Agent macro with customized options:

use Agent, restart: :transient, shutdown: 10_000

In the above code, the agent child specification is configured to have a restart strategy of :transient, meaning that the child process will only be restarted if it terminates abnormally. Additionally, the shutdown strategy is set to allow a grace period of 10,000 milliseconds for the child to shut down before forcefully terminating it.

Navigation

Home Supervised tasks Gotchas