Discussion about uses for Agent Processes

I am collecting examples of uses for Agent. I would love to hear uses you can share. Thanks in advance!

8 Likes

Oh I’d love to hear some good use cases too. I sort of feel guilty not using Agent at all. I usually grab GenServer instead without much thinking.

4 Likes

Yeah, same here, I always have ‘some’ functionality associated with shared state like that, so a GenServer has always seemed better, otherwise for shared state without functionality then I always use ETS/Mnesia instead… ^.^;

2 Likes

I have noticed one place that I tend to use it. I’ll share that later, so as not to bias the answers of others just yet…

2 Likes

I find it interesting that not many folks are chiming in with answers yet. I wonder if that signifies that they don’t get a ton of usage.

For myself, I have found them useful in testing other processes. I’ve rigged processes to pull pre-arranged values from them and stored values representing work happening inside of a process that I could later write assertions against.

The one time I tried to use them in an application, I grew to regret it and later replaced them with GenServer processes, for the functionality reasons noted by @OvermindDL1.

5 Likes

Oops, I did forget another decent usage I found for them!

I recorded the events of a simulation as they happened, then fetched and cleared all changes in the drawing code (using get_and_update()). That worked fine.

1 Like

I’ve been wondering the same thing myself. When do people actually use agent?

1 Like

I’d thought about using them as a low TTL query cache

1 Like

I don’t think I ever used Agent in one of my programs. That said, the use cases in the wild, I saw, are:

2 Likes

I also don’t use Agents for the production runtime code. I did use them on few occasions in tests.

1 Like

I’m using an agent like KV.bucket: http://elixir-lang.org/getting-started/mix-otp/agent.html. In the following function I’m using an agent as “accumulator”. If there’s a handier solution (I’m not as experienced as most posters) I would like to hear. :wink:

 @doc """
    Validate the input `body` in the taskform, task identified with `element_id', `socket` is third param
  """
  @spec validate_task(map, String.t, Socket.t) :: :ok
  def validate_task(body, element_id, socket) do
    Agent.start_link(fn -> [] end, name: :error_accumulator)

    traverse = fn(data, trav) ->
      case data do
        [_|_] ->
          Enum.map(data, &(trav.(&1,trav)))
        %{"key" => name, "validate" => validation, "label" => label} ->
          result = validate_item(body |> Map.fetch!(name),
                                 label,
                                 validation)
          if result != "", do: Agent.update(:error_accumulator, &List.insert_at(&1, 0, result))
        %{"components" => components} ->
           trav.(components,trav)
        %{"columns" => columns} ->
           trav.(columns,trav)
        %{"rows" => rows} ->
           trav.(rows, trav)
        _ ->
           data
      end
    end

    WorkflowAgent.get_user_tasks(socket.assigns.agent_pid)
    |> Stream.filter(fn task -> Map.fetch!(task, :id) === element_id end)
    |> Enum.at(0)
    |> Map.fetch!(:formfields)
    |> Enum.at(0)
    |> Map.fetch!("components")
    |> traverse.(traverse)

    errors = Agent.get(:error_accumulator, &(&1))

    Agent.stop(:error_accumulator, :normal)

    case errors do
      [] -> push socket, "cleanupFormIO", %{}
            :ok
      _  -> push socket, "alert",  %{message: "<br>" <> (errors |> List.foldl(" ", fn(x, acc) -> x <> acc end)), type: "error"}
            :errors_found
    end
  end
1 Like

You seem to be using agent here to simulate a mutable object. I don’t think that’s a good/idiomatic example for using agents (or processes for that matter).

It appears that traverse function here is used only to gather errors. You could write that in a pure functional way, by extracting a separate function which returns a list of errors. Maybe a function should be renamed to validate or something along those lines to express its main purpose.

Here’s a non-tested attempt:

defp validate([_|_] = data) do
  data
  |> Stream.map(&validate/1)
  |> Enum.concat()
end
defp validate(%{"key" => name, "validate" => validation, "label" => label}) do
  case validate_item(body |> Map.fetch!(name), label, validation) do
    "" -> []
    error -> [error]
  end
end
defp validate(%{"components" => components}), do: validate(components)
defp validate(%{"columns" => columns}), do: validate(columns)
defp validate(%{"rows" => rows}), do: validate(rows)
defp validate(_other_kind_of_data), do: []
5 Likes
You seem to be using agent here to simulate a mutable object. I don't think that's a good/idiomatic
example for using agents (or processes for that matter).

That’s wat I was troubled about. Your solution is perfect, thanks. I only had to add input param body.

5 Likes

I actually think this is what Agent is, and it is a reason why we seem to avoid it in our toolbox. Quite rightfully so as well.

It is basically a shared mutable state, and a such it’s usage should be controlled and limited.

3 Likes

I somewhere read the smart explanation: If you have only state, use agent, if you have only processing, use as task, if you have a mixture of both, use Genserver.

I am not using Agent since I prefer to have state in the db. We have the app in docker containers, so no erlang cluster. I think if I would use an erlang cluster then it would make more sense to use Agents to store state.

1 Like

I’ve been reading “Functional Web Development with Elixir and Phoenix” and I actually find the use cases over there quite appealing. You should check it out too.

EDIT: Ok, no I’m reading further and it does look awfully like I would defo go with GenServer.

1 Like

I understood that in reality it’s not shared mutable state, but I agree it’s a simulation. We need to persist things in memory sometimes (be it in ets, an agent, genserver). In my ‘accumulator’ case the use of an agent was not needed.

1 Like

I also find difficult to argue for the use of Agents. They were introduced as a solution to store data that is simpler than a GenServer. Often though, there is logic related to that data which makes a compelling argument to use a GenServer instead.
The only reason I would be tempted to use an agent is when I feel the need for an ETS table. This is where an agent should be the first solution before being labelled as a bottleneck.

2 Likes

I’m using GenServers and Agents to effectively wrap structs. The Agent handles the state transformation atomically via get_and_update/3 whilst the GenServer handles the logic around those transformations.

2 Likes

GenServers are already atomic though, their messages are fully serialized, so why have the extra Agent process?

1 Like