Do you want to pick up from where you left of?
Take me there

Poolboy

You can easily exhaust your system resources if you do not limit the maximum number of concurrent processes that your program can spawn. Poolboy is a widely used lightweight, generic pooling library for Erlang that addresses this issue.

Why use Poolboy?

Let’s think of a specific example for a moment. You are tasked to build an application for saving user profile information to the database. If you’ve created a process for every user registration, you would create an unbounded number of connections. At some point the number of those connections can exceed the capacity of your database server. Eventually your application can get timeouts and various exceptions.

The solution is to use a set of workers (processes) to limit the number of connections instead of creating a process for every user registration. Then you can easily avoid running out of your system resources.

That’s where Poolboy comes in. It allows you to easily set up a pool of workers managed by a Supervisor without much effort on your part. There are many libraries which use Poolboy under the covers. For example,redis_poolex (Redis connection pool) is a popular library which uses Poolboy.

Installation

Installation is a breeze with mix. All we need to do is add Poolboy as a dependency to our mix.exs.

Let’s create an application first:

mix new poolboy_app --sup

Add Poolboy as a dependency to our mix.exs.

defp deps do
  [{:poolboy, "~> 1.5.1"}]
end

Then fetch dependencies, including Poolboy.

mix deps.get

The configuration options

We need to know a little bit about the various configuration options in order to start using Poolboy.

Configuring Poolboy

For this example, we’ll create a pool of workers responsible for handling requests to calculate the square root of a number. We’ll keep the example simple so that we can keep our focus on Poolboy.

Let’s define the Poolboy configuration options and add the Poolboy worker pool as a child worker of our application. Edit lib/poolboy_app/application.ex:

defmodule PoolboyApp.Application do
  @moduledoc false

  use Application

  defp poolboy_config do
    [
      name: {:local, :worker},
      worker_module: PoolboyApp.Worker,
      size: 5,
      max_overflow: 2
    ]
  end

  def start(_type, _args) do
    children = [
      :poolboy.child_spec(:worker, poolboy_config())
    ]

    opts = [strategy: :one_for_one, name: PoolboyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

The first thing we defined is the configuration options for the pool. We named our pool :worker and set the :scope to :local. Then we designated PoolboyApp.Worker module as the :worker_module that this pool should use. We also set the :size of the pool to have total of 5 workers. Also, in case all workers are under load, we tell it to create 2 more workers to help with the load using the :max_overflow option. (overflow workers do go away once they complete their work.)

Next, we added :poolboy.child_spec/2 function to the array of children so that the pool of workers will be started when the application starts. It takes two arguments: name of the pool, and pool configuration.

Creating Worker

The worker module will be a simple GenServer that calculates the square root of a number, sleeps for one second, and prints out the pid of the worker. Create lib/poolboy_app/worker.ex:

defmodule PoolboyApp.Worker do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, nil)
  end

  def init(_) do
    {:ok, nil}
  end

  def handle_call({:square_root, x}, _from, state) do
    IO.puts("process #{inspect(self())} calculating square root of #{x}")
    Process.sleep(1000)
    {:reply, :math.sqrt(x), state}
  end
end

Using Poolboy

Now that we have our PoolboyApp.Worker, we can test Poolboy. Let’s create a simple module that creates concurrent processes using Poolboy. :poolboy.transaction/3 is the function that you can use to interface with the worker pool. Create lib/poolboy_app/test.ex:

defmodule PoolboyApp.Test do
  @timeout 60000

  def start do
    1..20
    |> Enum.map(fn i -> async_call_square_root(i) end)
    |> Enum.each(fn task -> await_and_inspect(task) end)
  end

  defp async_call_square_root(i) do
    Task.async(fn ->
      :poolboy.transaction(
        :worker,
        fn pid -> 
          # Let's wrap the genserver call in a try - catch block. This allows us to trap any exceptions
          # that might be thrown and return the worker back to poolboy in a clean manner. It also allows
          # the programmer to retrieve the error and potentially fix it.
          try do
            GenServer.call(pid, {:square_root, i})
          catch
            e, r -> IO.inspect("poolboy transaction caught error: #{inspect(e)}, #{inspect(r)}")
            :ok
          end
        end,
        @timeout
      )
    end)
  end

  defp await_and_inspect(task), do: task |> Task.await(@timeout) |> IO.inspect()
end

Run the test function to see the result.

iex -S mix
iex> PoolboyApp.Test.start()
process #PID<0.182.0> calculating square root of 7
process #PID<0.181.0> calculating square root of 6
process #PID<0.157.0> calculating square root of 2
process #PID<0.155.0> calculating square root of 4
process #PID<0.154.0> calculating square root of 5
process #PID<0.158.0> calculating square root of 1
process #PID<0.156.0> calculating square root of 3
...

If no worker is available in the pool, Poolboy will timeout after the default timeout period (five seconds) and won’t accept any new requests. In our example, we’ve increased the default timeout to one minute in order to demonstrate how we can change the default timeout value. In case of this app, you can observe the error if you change the value of @timeout to less than 1000.

Even though we’re attempting to create multiple processes (total of twenty in the example above) :poolboy.transaction/3 function will limit the maximum number of created processes to five (plus two overflow workers if needed) as we have defined in our configuration. All requests will be handled using the pool of workers rather than creating a new process for every request.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!