pool_sup v0.2.0 PoolSup

This module defines a supervisor process that is specialized to manage pool of workers.

Features

  • Process defined by this module behaves as a :simple_one_for_one supervisor.
  • Worker processes are spawned using a callback module that implements PoolSup.Worker behaviour.
  • PoolSup process manages which worker processes are in use and which are not.
  • PoolSup automatically restart crashed workers.
  • Functions to request pid of an available worker process: checkout/2, checkout_nonblocking/2.
  • Run-time configuration of pool size: change_capacity/2.

Example

Suppose we have a module that implements both GenServer and PoolSup.Worker behaviours (PoolSup.Worker behaviour requires only 1 callback to implement, start_link/1).

iex(1)> defmodule MyWorker do
...(1)>   @behaviour PoolSup.Worker
...(1)>   use GenServer
...(1)>   def start_link(arg) do
...(1)>     GenServer.start_link(__MODULE__, arg)
...(1)>   end
...(1)>   # definitions of gen_server callbacks...
...(1)> end

When we want to have 3 worker processes that run MyWorker server:

iex(2)> {:ok, pool_sup_pid} = PoolSup.start_link(MyWorker, {:worker, :arg}, 3, 0, [name: :my_pool])

Each worker process is started using MyWorker.start_link({:worker, :arg}). Then we can get a pid of a child currently not in use:

iex(3)> child_pid = PoolSup.checkout(:my_pool)
iex(4)> do_something(child_pid)
iex(5)> PoolSup.checkin(:my_pool, child_pid)

Don’t forget to return the child_pid when finished; for simple use cases PoolSup.transaction/3 comes in handy.

Reserved and on-demand worker processes

PoolSup defines the following two parameters to control capacity of a pool:

  • reserved (3rd argument of start_link/5): Number of workers to keep alive.
  • ondemand (4th argument of start_link/5): Maximum number of workers that are spawned on-demand.

In short:

{:ok, pool_sup_pid} = PoolSup.start_link(MyWorker, {:worker, :arg}, 2, 1)
w1  = PoolSup.checkout_nonblocking(pool_sup_pid) # => pre-spawned worker pid
w2  = PoolSup.checkout_nonblocking(pool_sup_pid) # => pre-spawned worker pid
w3  = PoolSup.checkout_nonblocking(pool_sup_pid) # => newly-spawned worker pid
nil = PoolSup.checkout_nonblocking(pool_sup_pid)
PoolSup.checkin(pool_sup_pid, w1)                # `w1` is terminated
PoolSup.checkin(pool_sup_pid, w2)                # `w2` is kept alive
PoolSup.checkin(pool_sup_pid, w3)                # `w3` is kept alive

Usage within supervision tree

The following code snippet spawns a supervisor that has PoolSup process as one of its children:

chilldren = [
  ...
  Supervisor.Spec.supervisor(PoolSup, [MyWorker, {:worker, :arg}, 5, 3]),
  ...
]
Supervisor.start_link(children, [strategy: :one_for_one])

The PoolSup process initially has 5 workers and can temporarily have upto 8. All workers are started by MyWorker.start_link({:worker, :arg}).

You can of course define a wrapper function of PoolSup.start_link/4 and use it in your supervisor spec.

Summary

Functions

Changes capacity (number of worker processes) of a pool

Checks in an in-use worker pid and make it available to others

Checks out a worker pid that is currently not used

Checks out a worker pid in a nonblocking manner, i.e. if no available worker found this returns nil

Query current status of a pool

Checks out a worker pid, executes the given function using the pid, and then checks in the pid

Types

options :: [{:name, GenServer.name}]
pool :: pid | GenServer.name

Functions

change_capacity(pool, new_reserved, new_ondemand)

Specs

change_capacity(pool, nil | non_neg_integer, nil | non_neg_integer) :: :ok

Changes capacity (number of worker processes) of a pool.

new_reserved and/or new_ondemand parameters can be nil; in that case the original value is kept unchanged (i.e. PoolSup.change_capacity(pool, 10, nil) replaces only reserved value of pool).

On receipt of change_capacity message, the pool adjusts number of children according to the new configuration as follows:

  • If current number of workers are less than reserved, spawn new workers to ensure reserved workers are available. Note that, as is the same throughout the OTP framework, spawning processes under a supervisor is synchronous operation. Therefore increasing reserved too many at once may make the pool unresponsive for a while.
  • When increasing total capacity (reserved + ondemand) and if any client process is being checking-out in a blocking manner, then the newly-spawned process is returned to the client.
  • When decreasing capacity, the pool tries to shutdown extra workers that are not in use. Processes currently in use are never interrupted. If number of in-use workers is more than the desired capacity, terminating further is delayed until any worker process is checked in.
checkin(pool, pid)

Specs

checkin(pool, pid) :: :ok

Checks in an in-use worker pid and make it available to others.

checkout(pool, timeout \\ 5000)

Specs

checkout(pool, timeout) :: pid

Checks out a worker pid that is currently not used.

If no available worker process exists, the caller is blocked until either

  • any process becomes available, or
  • timeout is reached.
checkout_nonblocking(pool, timeout \\ 5000)

Specs

checkout_nonblocking(pool, timeout) :: nil | pid

Checks out a worker pid in a nonblocking manner, i.e. if no available worker found this returns nil.

start_link(worker_module, worker_init_arg, reserved, ondemand, options \\ [])

Specs

start_link(module, term, non_neg_integer, non_neg_integer, options) :: GenServer.on_start

Starts a PoolSup process linked to the calling process.

Arguments

  • worker_module is the callback module of PoolSup.Worker.
  • worker_init_arg is the value passed to worker_module.start_link/1 callback function.
  • reserved is the number of workers this PoolSup process holds.
  • ondemand is the maximum number of workers that are spawned on checkouts when all reserved processes are in use.
  • Currently only :name option for name registration is supported.
status(pool)

Specs

status(pool) :: %{reserved: nni, ondemand: nni, children: nni, available: nni, working: nni} when nni: non_neg_integer

Query current status of a pool.

transaction(pool, f, timeout \\ 5000)

Specs

transaction(pool, (pid -> a), timeout) :: a when a: term

Checks out a worker pid, executes the given function using the pid, and then checks in the pid.

The timeout parameter is used only in the checkout step; time elapsed during other steps are not counted.