Implementing multiple pools
(This post is part of a series on writing a process pool manager in Elixir.)
In the last post, we did most of the work enabling us to have multiple pools within PoolToy. But before we jump into the implementation, let’s take a moment to think about fault tolerance: how should pool B be affected if pool A has a problem and crashes?
Well, the whole point of PoolToy is to keep those 2 pools independent: a crash in one of them shouldn’t affect the other in any way. At the same time, we’ve decreed that the pool manager and work supervisor are co-dependent and should therefore crash together: that’s why we’ve set the pool supervisor’s restart strategy to :one_for_all
(if the pool manager dies, we need the pool supervisor to kill off the worker supervisor, so a :one_for_one
strategy wouldn’t be a good fit).
Right, so within a pool, we need a :one_for_all
strategy, but between pools we need :one_for_one
. Therefore, we’ll need to introduce another level of supervision: a pools supervisor (note the plural: not to be confused with the singular pool supervisor) will supervise the various individual pool supervisors.
Adding PoolsSup
Here’s our new PoolsSup
module (lib/pool_toy/pools_sup.ex
):
defmodule PoolToy.PoolsSup do
use DynamicSupervisor
@name __MODULE__
def start_link(opts) do
DynamicSupervisor.start_link(__MODULE__, opts, name: @name)
end
def init(_opts) do
DynamicSupervisor.init(strategy: :one_for_one)
end
end
Since we’re now going to handle starting pools through the pools supervisor, that’s what our application module should start instead of a pool supervisor (lib/pool_toy/application.ex
):
defmodule PoolToy.Application do
use Application
def start(_type, _args) do
children = [
PoolToy.PoolsSup
]
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
end
end
On line 6, we’re no longer directly starting a pool supervisor, but start the pools supervisor. And since we’ve now got a statically named process, let’s go ahead and register that on lines 4-6 (mix.exs
):
def application do
[
mod: {PoolToy.Application, []},
registered: [
PoolToy.PoolsSup
],
extra_applications: [:logger]
]
end
Let’s check everything works properly: start an IEx session with iex -S mix
. If you take a look at the Observer (launch it with :observer.start()
, remember?) you’ll that no pools were started, because our application file no longer launches a pool on startup, it only starts the pools supervisor.
But we can take our code for a spin by starting a new pool ourselves: the pools supervisor is a DynamicSupervisor, so we can use DynamicSupervisor.start_child/2
to do so:
DynamicSupervisor.start_child(PoolToy.PoolsSup,
{PoolToy.PoolSup, [name: :poolio, worker_spec: Doubler, size: 3]})
Calling that manually is a bit of a pain, especially since we’re expecting the client to magically know he’s dealing with a DynamicSupervisor. Let’s clean that up by adding PoolsSup.start_pool/1
(lib/pool_toy/pools_sup.ex
):
def start_pool(args) do
DynamicSupervisor.start_child(@name, {PoolToy.PoolSup, args})
end
def init(_opts) do
# edited for brevity
If you now start a new IEx session, you can start a new pool with
iex(1)> PoolToy.PoolsSup.start_pool(name: :poolio, worker_spec: Doubler, size: 3)
Let’s make that even better by exposing our public API through the PoolToy
module (lib/pool_toy.ex
):
defmodule PoolToy do
defdelegate start_pool(args), to: PoolToy.PoolsSup
end
Note the generated code previously in the PoolToy
module has been completely removed. As you can tell from the docs (and probably its name), defdelegate/2
simply defines a function on the current module that will delegate the call to a function defined in another module.
And now we can start pools in an even cleaner fashion:
iex(1)> PoolToy.start_pool(name: :poolio, worker_spec: Doubler, size: 3)
Great! Let’s start another pool, since it’s so easy!
iex(2)> PoolToy.start_pool(name: :pooly, worker_spec: Doubler, size: 3)
{:error,
{:shutdown,
{:failed_to_start_child, PoolToy.PoolMan,
{:badarg,
[
{:ets, :new, [:monitors, [:protected, :named_table]], []},
{PoolToy.PoolMan, :init, 2, [file: 'lib/pool_toy/pool_man.ex', line: 70]},
{:gen_server, :init_it, 2, [file: 'gen_server.erl', line: 374]},
{:gen_server, :init_it, 6, [file: 'gen_server.erl', line: 342]},
{:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 249]}
]}}}}
Dang. It was too good to be true, wasn’t it? Try figuring out the problem on your own, and join me in next post to fix multiple pool creation.
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.