03 October 2013

Why is a pool manager required?

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.

Building an application with pooled processes to say hello

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:

  • Name of the pool.
  • Options for the pool. This is where you specify the name to register the workers as, size of the pool, etc
  • Work arguments to be passed on to the worker when it is started. Like initial state, etc. These are passed to the 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):

  • name of the worker module for the processes in the pool
  • size is the number of processes in the pool
  • max_overflow is the number of process that is allowed as an excess

If 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

Playing with your workers

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 pool

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

What happens if we never check back workers to Poolboy?

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.

Being a responsible borrower

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

  • name of the pool
  • functon that takes a worker pid as an arugment and performs whatever task required

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.

Notes

  • Source code for the above example application is on Github.
  • There are more variations of :poolboy.checkout and :poolboy.transaction functions. I’ve just kept it simple.
  • There is a more detailed example in the readme of the Poolboy repo.
  • Seth Falcon wrote Pooler which is another process pooling tool.

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.

blog comments powered by Disqus