When building an application, you might have processes that perform time consuming tasks. Process pools can be used in these cases.
With process pools, you can have a pool of processes started up and ready to serve. You can then send messages to them whenever you want them to do something.
With a pool manager you can make sure that, no more than a set limit of processes can be spawned.
Poolboy is a pool manager written by Devin Torres.
We’ll build a mission critical greeting application. It is compulsory that we use the most boring name for our application, so I’ll call it “pool_management”
mix new pool_management --sup
Add Poolboy to the list of project dependencies in mix.exs
defp deps do
[ {:poolboy, github: "devinus/poolboy", tag: "1.0.0"} ]
end
Add a worker at lib/pool_management/hello_worker.ex
defmodule PoolManagement.HelloWorker do
use GenServer.Behaviour
def start_link(state) do
:gen_server.start_link(__MODULE__, state, [])
end
def init(state) do
{:ok, state}
end
def handle_call(:greet, from, state) do
{:reply, "Hello from #{self |> pid_to_list}", state}
end
end
pid_to_list
returns the passed pid as a string. The man page warns that this function is only for internal use. We are using it here for demonstration purposes only.
The above is a very simple example for GenServer.Behaviour
. When you send the message :greet
it replies back with a greeting with it’s pid. We’ll now use Poolboy to create a pool of HelloWorkers.
You’ll need to inform the supervisor about the child specification of the process pool. Poolboy provides a function for this - :poolboy.child_spec
. It expects the following arguments:
start_link
function of the worker module you specify. I find no need for the worker arguments right now, so I’m passing an empty list.Here’s a list of the commonly used options (the second argument):
size
is the number of processes in the poolmax_overflow
is the number of process that is allowed as an excessIf the size of the pool is 5, and the max_overflow
is 10, then you can get a total of 15 proceses from the pool. (It makes you an offer you cannot refuse). Any more than that, Poolboy will be give you nothing but timeouts.
This is what lib/pool_management/supervisor.ex
will look like
defmodule PoolManagement.Supervisor do
use Supervisor.Behaviour
def start_link do
:supervisor.start_link(__MODULE__, [])
end
def init([]) do
# Here are my pool options
pool_options = [
name: {:local, :hello_pool},
worker_module: PoolManagement.HelloWorker,
size: 5,
max_overflow: 10
]
children = [
:poolboy.child_spec(:hello_pool, pool_options, [])
]
supervise(children, strategy: :one_for_one)
end
end
Poolboy has 2 handy functions:
:poolboy.checkout(pool_name)
returns a worker pid:poolboy.checkin(pool_name, worker)
checks the worker back into the process poolStart up the mix console with iex -S mix
. Your application should have already started.
$ iex -S mix
iex(1)> worker = :poolboy.checkout(:hello_pool)
#PID<0.68.0>
iex(2)> :gen_server.call worker, :greet
"Hello from <0.68.0>"
iex(3)> :poolboy.checkin(:hello_pool, worker)
:ok
You requested Poolboy for a worker, used it and gave it back - just what a good kid would do.
We already used one worker and checked it back in. Which means, our entire pool size of 5 is still up for use by us. Let’s waste them.
iex(4)> Enum.map(1..5, fn(i) -> :poolboy.checkout(:hello_pool) end)
[#PID<0.68.0>, #PID<0.69.0>, #PID<0.70.0>, #PID<0.71.0>, #PID<0.72.0>]
It returned 5 pids. Let’s use-up the overflow quota too.
iex(5)> Enum.map(1..10, fn(i) -> :poolboy.checkout(:hello_pool) end)
[#PID<0.74.0>, #PID<0.75.0>, #PID<0.76.0>, #PID<0.77.0>, #PID<0.78.0>,
#PID<0.79.0>, #PID<0.80.0>, #PID<0.81.0>, #PID<0.82.0>, #PID<0.83.0>]
Now you’ve seen what the max_overflow
option that you pass to :poolboy.child_spec
means. We have now used up the overflow quota of processes too. What happens when we request another?
iex(6)> :poolboy.checkout(:hello_pool)
** (exit) {:timeout, {:gen_server, :call, [:hello_pool, {:checkout, true, 5000}, 5000]}}
gen_server.erl:188: :gen_server.call/3
erl_eval.erl:569: :erl_eval.do_apply/6
src/elixir.erl:138: :elixir.eval_forms/3
Poolboy starts to ignores us. It now knows we’ve gone rogue.
After you request a worker, say the process that requested the worker crashes. That’s one process that wasn’t checked back in, which means your remaining pool size is one less.
To avoid this, Poolboy provides another function - :poolboy:transaction
. It accepts two arguments
Here’s an example from the console
$ iex -S mix
iex(1)> :poolboy.transaction(:hello_pool, fn(worker)-> :gen_server.call(worker, :greet) end)
"Hello from <0.71.0>"
If the function crashes for some reason, Poolboy knows about it and can take action about the requested worker.
:poolboy.checkout
and :poolboy.transaction
functions. I’ve just kept it simple.You can build your own simple pool manager with whatever OTP provides, but that’s a play for another day.
I’m loving Elixir. I’ve been playing with it for a couple weeks now. If you are interested in such stuff, I’m @HashNuke on twitter.