2
votes

So looking at examples, I'm a bit confused:

The elixir docs (http://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html#supervision-trees) seem to suggest you can build supervision trees with just a "use Supervisor" macro.

But all the examples I see in blog posts / random Internet searches "use Genserver". So does a supervision tree have to use the Genserver interface?

I think I need some pointers in the right direction, maybe code to look at or a clearer example.

2
Which example are you referring to? That page doesn't have a single use GenServer.Dogbert
Edited the question for clariy @Dogbert, you're right that page doesn't use GenServer but lots of internet examples do, so I'm confused on how to make supervision trees work w/o GenServer or if that's even possible.Nona
You shouldn't use Supervisor if you're using GenServer, because they use the same callbacks (init etc). A Supervisor is very different of a Genserver: 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 use simple_one_for_one supervisors, which in turn will spawn your GenServer 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

2 Answers

2
votes

Designing the supervision tree in a complex application can be one of the most challenging aspect of architecting an opt application. However, for the most part, I find that most applications done need that complexity.

First, so mix the concept of supervisors with the processes they supervise. i.e. GenServers, Agents, Tasks, GenFSMs, etc.

For many simple apps/web apps, a flat supervisor tree will work. The work flow goes like this;

  • Add a new GenServer module
  • Add the startup of that module to your main supervisor worker(NewServer, [])

However, for more complicated solutions you may want to finer control of which servers are linked when one fails. This is where you would introduce another layer to the supervisor tree. In this case, you would add a new supervisor module and start it like

# application
children = [
  worker(WorkerMod1, args),
  worker(WorkerMod2, args2),
  supervisor(NextLevelSup, sargs)
]
# ....

# next_level_sup
children = [
  worker(GenServer1Grp1, args, id: "some unique id", restart: :temporary),
  worker(GenServer2Grp2, args2, id: "id2", restart: :transient),
]
# ...

Then choose the restart strategy you want to use for the new supervisor.

tl;dr Each server goes in a module. Each server is started from a supervisor in a different module.

One of my Elixir apps (the first one I designed) Has a number of levels of supervision. You can find more information on this video TS 12:10

1
votes

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.