Elixir Pooling with Poolboy

Poolboy is a generic pooling library for Erlang (ergo Elixir). Since it is originally made for Erlang, you wouldn’t find a great set of documentation around how to use it on Elixir apps, and especially with Phoenix apps. If you know your way around Erlang, you can follow the wonderful documentation on their readme. If not, this post will try to explain just the basics of how you could get started and include this in your project.

First things first, do you need pooling? If you are spinning up processes that could take a while to run, you could soon run into a problem where there is a huge wave of processes being spinned up without the previous ones finishing and thereby exhausting all your system resources. BEAM is great at distributing work across the processes and it will try to do its best, but even it is limited by system resources. This is where Poolboy comes in.

The installation is simple, something like {:poolboy, "~> 1.5"} in your mix.exs would normally be enough. Now, in order to use Poolboy, you would first need to identify the resource that you need to pool. If you have a static module that you spin up to perform some task, e.g. an open connection to a database (all of the following is imaginary code), something like the following:

defmodule MyApp.Processor do
  def process(arg1, arg2) do
		{:ok, connection} = DBConnection.open()
		DBConnection.do_something(connection, arg1, arg2)
	end
end

There are two big issues with the above code.

  1. You are opening a new connection to the DB whenever you want to process something.
  2. If you have 1000s of requests at the same time, you might soon run out of memory if your DB does not allow that many simultaneous connections.

This is of course an imaginary situation and most DB libraries out there would probably manage this for you. But this could be extended to any number of use cases, for example, opening a ErlPort to a ruby process and communicating with it.

Let’s see how we can pool this operation. First, we will need to make the module stateful. The easiest way to do that is to create an Agent. Let’s see how the agent would look like:

defmodule MyApp.ProcessorAgent do
	def start_link(_params) do
    Agent.start_link(fn ->
      {:ok, connection} = DBConnection.open()
      connection
    end)
  end

	def process(pid, arg1, arg2) do
		Agent.get(pid, fn connection ->
      DBConnection.do_something(connection, arg1, arg2)
    end)
	end
end

Simple enough, right? We create a new state in start_link and then whenever we need to process something, we get the agent from it’s PID and do something with the connection (that is the state of the agent). We can now update our original Processor to use this agent instead of directly opening a new connection.

defmodule MyApp.Processor do
  def process(arg1, arg2) do
		:poolboy.transaction(:processor_agent, fn pid ->
      MyApp.ProcessorAgent.process(pid, arg1, arg2)
    end)
	end
end

This uses the :poolboy.transaction/3 to use a checkout a process from a pool and run the processing logic inside that. You might be wondering what is special about the :processor_agent atom. Nothing, this is just a name that we give to the process pool when starting our application. In application.ex, do the following inside your start/2 method:

def start(_type, _args) do
	children = [
		# ...
		:poolboy.child_spec(:processor_agent, processor_poolboy_config())]
	]
end

def processor_poolboy_config() do
	[
		name: {:local, :processor_agent},
		worker_module: MyApp.ProcessorAgent,
		size: 5, # Initial number of workers
		max_overflow: 2 # Extra workers (auto-stopped after work) to spawn if under load
	]
end

There are a few other options to configure Poolboy. Now that you know how the interface with Elixir should look like, I will leave you to explore the configuration options on Poolboy.

Published 18 Apr 2021

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter