Managing a single pool (continued)
(This post is part of a series on writing a process pool manager in Elixir.)
In the previous post, we managed to get our pool supervisor to start the pool manager. Now, our next objective is to have the pool supervisor start the worker supervisor, and we’ll be one step closer to out intermediary goal:
Before we just dive in, let’s think about our worker supervisor a bit and how it differs from the pool supervisor. The worker supervisor’s children are all going to be the same process type (i.e. all workers based on the same module), whereas the pool supervisor has different children. Further, the number of children under the worker supervisor’s care will vary: in the future, we’ll want to be able to spin up temporary “overflow” workers to handle bursts in demand. These features make it a great fit for the DynamicSupervisor.
The worker supervisor
Here’s our bare-bones implementation of the worker supervisor (lib/pool_toy/worker_sup.ex
):
defmodule PoolToy.WorkerSup do
use DynamicSupervisor
@name __MODULE__
def start_link(args) do
DynamicSupervisor.start_link(__MODULE__, args, name: @name)
end
def init(_arg) do
DynamicSupervisor.init(strategy: :one_for_one)
end
end
This should look familiar: it’s not very different from the code we wrote for the pool supervisor. The main differences aside from use
ing DynamicSupervisor
on line 2 are the restart strategy on line 11 (DynamicSupervisor
only supports :one_for_one
at this time), and the fact that we don’t initialize any children. Dynamic supervisors always start with no children and add them later dynamically (hence their name).
We still need our pool supervisor to start the worker supervisor (lib/pool_toy/pool_sup.ex
):
def init(args) do
pool_size = args |> Keyword.fetch!(:size)
children = [
{PoolToy.PoolMan, pool_size},
PoolToy.WorkerSup
]
Supervisor.init(children, strategy: :one_for_all)
end
Since this time around we don’t need to pass any special values for the initialization, we don’t need a tuple and can just pass in the module name directly. A quick detour in IEx to try everything out:
iex -S mix
PoolToy.PoolSup.start_link(size: 4)
:observer.start()
In the processes tab, you can see we’ve got entries for PoolSup
, PoolMan
, and WorkerSup
. In addition, if you double click on the pool manager and worker supervisor entries and navigate to their “state” tabs, you’ll see that they both have the pool supervisor as their parent. Great (intermediary) success!
Throwing the OTP application into the mix
Poking around the Observer using the process list has been helpful, but it’d be even better if we could have a more visual representation of the hierarchy between our processes. You may have noticed the “applications” tab in the Observer: it has pretty charts of process hierarchies, but our processes are nowhere to be found. It turns out, that’s because right now our code is just a bunch of processes, they’re not an actual OTP application.
First of all, what is an OTP application? Well, it’s probably not the same size/scope as the application you’re thinking of: OTP applications are more like components in that they’re bundles of reusable code that gets started/stopped as a unit. In other words, the software you build in OTP (whether it’s with Elixir, Erlang, or something else) will nearly always be contain or depend on several OTP applications.
To turn our project into an application, we need to first write the application module callback (lib/pool_toy/application.ex
):
defmodule PoolToy.Application do
use Application
def start(_type, _args) do
children = [
{PoolToy.PoolSup, [size: 3]}
]
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
end
end
Pretty straightforward, right? This code once again closely resembles the code we’ve been writing thus far, except we’re using Application
on line 2 and the callback we needed to implement is start/2
.
You’ll probably have noticed that we’ve hard-coded a pool size of 3 on line 6. That’s just temporary to get us up and running: in our final implementation, the application will start a top-level supervisor and we’ll once again be able to specify pool sizes as we create them.
So now that we’ve defined how our application should be started, we still need to wire it up within mix so it gets started automatically (mix.exs
):
def application do
[
mod: {PoolToy.Application, []},
registered: [
PoolToy.PoolSup,
PoolToy.PoolMan,
PoolToy.WorkerSup
],
extra_applications: [:logger]
]
end
Line 3 is where the magic happens: we specify the mod
ule that is the entry point for our application, as well as the start argument. Here, that means the application gets started by calling the start/2
callback in PoolToy.Application
and providing it []
as the second argument (the first argument is used to specify how the application is started, which is useful in more complex failover/takeover scenarios which won’t be covered here).
You can safely ignore the registered
key/value here: I’ve only included it for completeness. It’s essentially a list of named processes our application will register. The Erlang runtime uses this information to detect name collisions. If you leave it out, it will default to []
and your app will still work.
Start an IEx session once again (using iex -S mix
, remember?), fire up the Observer with :observer.start()
, and check out what we can see in the “applications” tab:
Pretty sweet, right? (If you don’t see this, click on “pool_toy” on the left.)
Now that we’ve got a visual representation of what the relationships between our processes look like, let’s mess around with them a bit… Double click on each of our processes, take note of their pids (visible in the window’s title bar), and close their windows. Now, right click on PoolMan
and select “Kill process” (press “ok” when prompted for the exit reason). Look at the pid for PoolMan
by double clicking on it again: it’s different now, since it was restarted by PoolSup
. If you look at WorkerSup
‘s pid, you’ll see it’s also different: PoolSup
restarted it as well, because we told it to use a :one_for_all
strategy. Ah, the magic of Erlang’s supervision trees…
Starting new projects as OTP applications
We went about writing our application in a bit of a round about way: we wrote our code, and introduced the OTP application when we needed/wanted it by writing the application callback module ourselves and adding to mix.exs
.
Naturally, Elixir can helps us out here when we’re starting a new project. By passing the --sup
option to mix new
, which will create a skeleton of an OTP application with a supervision tree (docs). So we could have saved ourselves some work by starting out our project with
mix new pool_toy --sup
The more you know!
Our application is starting to look like the figure at the top of the post, but we still need actual worker processes. Let’s get to that next!
Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.