Subscribe now

Supervising Tasks and Agents [03.02.2017]

Tasks and Agents are both built on GenServer. Tasks are purely computation, and Agents are purely state management. For everything in between, there's GenServer.

Agents and Tasks can both be killed. Consequently, they can both be supervised if you'd like them to be restarted in the event of failure. Let's check it out.

Project

We're going to begin a new project:

mix new --sup agent_task_supervision_playground

Tasks

We'll start by looking at tasks. Most of the time, you'll create a task as a one-off operation. You can do this with Task.async or Task.start_link. In either of these cases, the tasks will be linked to the process that started them.

You might not want this. For instance, perhaps you want to kick off a task that outlives the process that started it. Off the top of my head, an example would be a task that is started as a result of a web request - the request is handled in its own process, so when the request is completed, the Task will see this because it is linked, and the Task will terminate.

To handle this use case and others, you can use Task.Supervisor. This spawns the task under a supervision tree, so that it outlives the process that started it. Let's write a quick test that serves as an example:

vim test/agent_task_supervision_playground_test.exs
defmodule AgentTaskSupervisionPlaygroundTest do
  use ExUnit.Case

  test "tasks that outlive their spawner" do
    pid = self()
    spawn(fn() ->
      Task.start_link(fn() ->
        :timer.sleep 50
        send(pid, :sup)
      end)
      Process.exit(self(), :kill)
    end)
    :timer.sleep 60
    assert_receive :sup
  end
end

If we run this test, it will fail. This is because the spawned function dies before the task it started. Let's start a Task.Supervisor in our supervision tree. We can then use that to spawn our supervised task.

vim lib/agent_task_supervision_playground/application.ex
defmodule AgentTaskSupervisionPlayground.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # Add a Task.Supervisor to our supervision tree, named OurSupervisor
      supervisor(Task.Supervisor, [[name: OurSupervisor]])
    ]

    opts = [strategy: :one_for_one, name: AgentTaskSupervisionPlayground.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Now we have a Task.Supervisor running in our supervision tree. We can use Task.Supervisor.start_child to start a task under this supervisor any time we'd like. Let's modify our test:

defmodule AgentTaskSupervisionPlaygroundTest do
  use ExUnit.Case

  test "tasks that outlive their spawner" do
    pid = self()
    spawn(fn() ->
      Task.Supervisor.start_child(OurSupervisor, fn() ->
        :timer.sleep 50
        send(pid, :sup)
      end)
      Process.exit(self(), :kill)
    end)
    :timer.sleep 60
    assert_receive :sup
  end
end

Now it passes. A Task.Supervisor is started by default in :simple_one_for_one mode with temporary workers. This means that if a supervised task crashes, it will not be restarted. You can pass different options when starting your Task.Supervisor if you would like different behaviour.

If you want to produce asynchronous computation without any need for state management, supervised Tasks are a great choice. If you need both state and computation, you'll want to use a GenServer.

You might ask why you shouldn't just spawn a process to do throwaway tasks like this. Ultimately, you want your processes to play nicely with OTP. This will make your life drastically easier if you encounter problems with them down the road or need to inspect them in a running system - OTP processes do a lot of small bookkeeping work that help a lot when it comes time to manage things operationally.

Agent

Similarly, Agents may be supervised. Let's add a basic test for an Agent:

defmodule AgentTaskSupervisionPlaygroundTest do
  use ExUnit.Case
  # ...
  test "working with an agent" do
    {:ok, _} = Agent.start_link(fn() -> [] end, [name: OurAgent])
    Agent.update(OurAgent, fn(state) -> [:foo|state] end)
    assert :foo = Agent.get(OurAgent, fn(state) -> hd(state) end)
  end
end

This is basic Agent usage. Here we started the Agent in our test and gave it a name. We can start the Agent when our app starts instead, placing it in the supervision tree. To do that, we need to pass our name argument to Agent.start_link. This cannot be done by just calling Agent inside of worker, so we need to build a module that will wrap our Agent:

vim lib/agent_task_supervision_playground/bucket.ex
defmodule AgentTaskSupervisionPlayground.Bucket do
  def start_link(name) do
    Agent.start_link(fn() -> [] end, [name: name])
  end

  def push(pid, item) do
    Agent.update(pid, fn(state) -> [item|state] end)
  end

  def head(pid) do
    Agent.get(pid, fn(state) -> hd(state) end)
  end
end

Now we can start a bucket by giving it a name. Let's modify our test to use this module first:

defmodule AgentTaskSupervisionPlaygroundTest do
  # ...
  alias AgentTaskSupervisionPlayground.Bucket
  # ...
  test "working with an agent" do
    {:ok, _} = Bucket.start_link(OurBucket)
    Bucket.push(OurBucket, :foo)
    assert :foo = Bucket.head(OurBucket)
  end
end

Now we can start our bucket in the supervision tree and quit starting it inside our test:

vim lib/agent_task_supervision_playground/application.ex
defmodule AgentTaskSupervisionPlayground.Application do
  # ...
  alias AgentTaskSupervisionPlayground.Bucket

  def start(_type, _args) do
    # ...
    children = [
      # ...
      worker(Bucket, [OurBucket])
    ]
    # ...
  end
end
  test "working with an agent" do
    Bucket.push(OurBucket, :foo)
    assert :foo = Bucket.head(OurBucket)
  end

If you run the tests now, they pass. Now OurBucket will be running any time our application is running. We were able to make something roughly equivalent to our FridgeServer using Agent and gain the same guarantees regarding supervision, with less boilerplate around GenServer. If you have modules that are solely concerned with state management, using a supervised Agent with a module wrapping it to provide a pleasant API is a great choice. If you need your server to do any computation, though, reach for a GenServer.

Summary

Today we saw how to use Agents and Tasks inside of our OTP Supervision Trees. There's a lot you could play with - using different restart modes for a particular Task.Supervisor might make sense for different purposes, for example. See you soon!

Resources