pool_sup v0.2.3 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/3.
  • Load-balancing using multiple pools: PoolSup.Multi.

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)> worker_pid = PoolSup.checkout(:my_pool)
iex(4)> do_something(worker_pid)
iex(5)> PoolSup.checkin(:my_pool, worker_pid)

Don’t forget to return the worker_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 for the subsequent checkout
PoolSup.checkin(pool_sup_pid, w3)                # `w3` is kept alive for the subsequent checkout

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 up to 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

See PoolSup.CustomSupHelper.format_status/2

Query current status of a pool

See PoolSup.CustomSupHelper.terminate/2

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, the pool spawns 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 maximum 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.

format_status(opt, list)

See PoolSup.CustomSupHelper.format_status/2.

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.

terminate(reason, state)

See PoolSup.CustomSupHelper.terminate/2.

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.