To answer the question in the title, use Supervisor
is sufficient on its own.
I think what's confusing about the chapter you mentioned in the Elixir Mix and OTP guide is that most of the examples are for an example project that effectively ends up implementing some of what Supervisor
is more typically (and easily) used for.
A supervisor is probably most clearly understood as something like a special GenServer
; Supervisor
itself calls a few GenServer
functions:
A GenServer
is a 'generic server'. A Supervisor
is something like a special server that (almost always) only supervises other servers (including other supervisors).
But because supervisors are special, they're typically only concerned with supervising other 'generic' servers and don't themselves encapsulate any other logic.
The reason why you'll often see "use GenServer
" in examples in many blog posts and search results is that GenServer
is much more generally useful, for all of the effectively infinite number of possible 'generic servers' one might want. A supervisor or even an entire supervision tree, is much more boring as it mostly just handles restarting supervised processes when they crash.
You might have a pool of workers for interacting with, e.g. an external service. Those workers would very likely be nicely implemented with use GenServer
(or use Agent
). Some other components of your app need to interact with those workers, e.g. to give them work. That should be encapsulated as its own process, e.g. as a GenServer
(or Registry
).
To ensure that workers are restarted if (when) they crash, and to similarly ensure that the registry is restarted if it crashes, you'd use a supervisor to do so.
In the Elixir Mix and OTP guide, the running example is of a distributed key-value store. The first chapter is mostly just an intro to Mix.
The second chapter tho covers managing state in a distributed system, e.g. concurrently. It uses Agent
, which is basically a process with some state that can be both retrieved and updated.
The third chapter covers GenServer
but note carefully that the basic key-value store implemented with Agent
in the previous chapter isn't modified to use GenServer
. Instead, this chapter covers building a "process registry", i.e. a way to manage multiple key-value stores and to access them by name. Note that most of its features could be implemented with Agent
too (which is itself built on GenServer
).
The registry is also effectively a separate key-value store – where the keys are the names of the other key-value stores and the values are references to the running processes for those other stores.
The end of the chapter covers process monitoring and uses it to handle the case of an (Agent
) key-value store process crashing. Without monitoring those processes, there's no way to know when they've crashed and thus when to remove those stores (processes) from the registry. Using GenServer
makes it easy to handle those monitored messages.
The very end of the chapter discusses tho why that implementation isn't very good [emphasis mine]:
Returning to our handle_cast/2
implementation, you can see the registry is both linking and monitoring the buckets:
{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)
This is a bad idea, as we don’t want the registry to crash when a bucket crashes. The proper fix is to actually not link the bucket to the registry. Instead, we will link each bucket to a special type of process called Supervisors, which are explicitly designed to handle failures and crashes. We will learn more about them in the next chapter.
Supervisors are so boring you might not ever even need to write a supervisor module and use Supervisor
in it. You might be perfectly served by just defining supervisors in your Application
module. Here's the start/2
function from the application module of one of my apps:
def start(_type, _args) do
children = [
# Start the Ecto repository
MyApp.Repo,
# Start the Telemetry supervisor
MyAppWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: MyApp.PubSub},
# Start the Endpoint (http/https)
MyAppWeb.Endpoint,
# Start a worker by calling: MyApp.Worker.start_link(arg)
# {MyApp.Worker, arg}
{DynamicSupervisor, strategy: :one_for_one, name: :agent_supervisor}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
You can see that my app's Ecto
repository has its own process, as does some other standard components, e.g. Telemetry
, Phoenix.PubSub
, and my app's web endpoint. And those processes are all themselves supervisors, possibly even of fairly dense 'trees' of processes.
I've also added a dynamic supervisor named :agent_supervisor
using the standard DynamicSupervisor
. The idea with it is to manage any number (i.e. a dynamic number) of agents; the same could be used for GenServer
modules/processes too. If I decide (for some reason) to add some logic to that dynamic supervisor, I could create a MyApp.AgentSupervisor
module, use DynamicSupervisor
in it, and change the line (the 'child spec') in my application start/2
function:
{DynamicSupervisor, strategy: :one_for_one, name: :agent_supervisor}
to just:
MyApp.AgentSupervisor
Interestingly, in one professional instance, I implemented a 'messy' supervisor with use GenServer
because the actual supervision 'strategy' I needed wasn't covered by the basic Supervisor
features.
use GenServer
. – DogbertSupervisor
if you're usingGenServer
, because they use the same callbacks (init
etc). ASupervisor
is very different of aGenserver
: What you usually do is keep the supervisors as simple as you can (As few logic as possible), because they will be monitoring your GenServer based modules. When you need dynamic supervision trees, you'll usesimple_one_for_one
supervisors, which in turn will spawn yourGenServer
based modules as their child. If you'd give an example of something you want, it would be easier for us to describe possible ways to do so. – Doodloo