[003.3] Supervising Tasks and Agents

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

defmoduleAgentTaskSupervisionPlaygroundTestdouseExUnit.Casetest"tasks that outlive their spawner"dopid=self()spawn(fn()->Task.start_link(fn()->:timer.sleep50send(pid,:sup)end)Process.exit(self(),:kill)end):timer.sleep60assert_receive:supendend

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

defmoduleAgentTaskSupervisionPlayground.Applicationdo@moduledocfalseuseApplicationdefstart(_type,_args)doimportSupervisor.Spec,warn:falsechildren=[# Add a Task.Supervisor to our supervision tree, named OurSupervisorsupervisor(Task.Supervisor,[[name:OurSupervisor]])]opts=[strategy::one_for_one,name:AgentTaskSupervisionPlayground.Supervisor]Supervisor.start_link(children,opts)endend

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:

defmoduleAgentTaskSupervisionPlaygroundTestdouseExUnit.Casetest"tasks that outlive their spawner"dopid=self()spawn(fn()->Task.Supervisor.start_child(OurSupervisor,fn()->:timer.sleep50send(pid,:sup)end)Process.exit(self(),:kill)end):timer.sleep60assert_receive:supendend

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:

defmoduleAgentTaskSupervisionPlaygroundTestdouseExUnit.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)endend

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:

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

defmoduleAgentTaskSupervisionPlaygroundTestdo# ...aliasAgentTaskSupervisionPlayground.Bucket# ...test"working with an agent"do{:ok,_}=Bucket.start_link(OurBucket)Bucket.push(OurBucket,:foo)assert:foo=Bucket.head(OurBucket)endend

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

test"working with an agent"doBucket.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!

sign up for full access

Meet your expert

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.