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