Building a pool manager, part 1.3

Posted on July 10, 2018

 This article is part of a series on writing a process pool manager in Elixir.

Managing a single pool (continued)

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:

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 useing 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

iex> PoolToy.PoolSup.start_link(size: 4)
iex> :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!

Our changes so far

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 module 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.

Our changes so far

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!

 This article is part of a series on writing a process pool manager in Elixir.


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.