Hi, I’ve written a pooler using gen_statem, similar to poolboy but with a few differences, more control on how it can be configured (default workers, max workers (similar to overflow in poolboy) and waiting queue lenght), some more control on checking out the worker (immediate overload response, only timeout, unbound, not swallowing timeouts).
I’ve ran some tests with it and poolboy and the results are promising. What I’m wondering is if there’s any other more efficient poolers out there? Perhaps nif based? I’m asking because if not I would like to rewrite it in erlang (it’s mostly compatible with only some keyword lists for initialisation and naming which can be swapped for something else) and publish it publicly (and because poolboy is used in some public elixir libs if it was a better option it would be, well, better?).
They have slightly different semantics on how they control, the things but running poolboy with no “block”, and an :infinity
timeout, against the unbound waiting queue
, and 5_000 timeout, both with 100 workers, not overflow, results in this:
Interactive Elixir (1.9.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Queuer.Pool.test_queuer()
:ok
Total ok: 40000 - Total failed: 0
time: 1.13
iex(2)> Queuer.Pool.test_poolboy()
:ok
Total ok: 40000 - Total failed: 0
time: 41.639
The results are consistent across all range of workloads, workers, etc.
The tests are basically:
@total_reqs 20000
def test_queuer(max \\ @total_reqs) do
pid = Process.whereis(__MODULE__)
time = :erlang.system_time(:millisecond)
receiver = spawn(__MODULE__, :keep_track, [time, 0, 0])
feed_test_queuer(pid, receiver, 0, max)
feed_test_queuer(pid, receiver, 0, max)
end
def feed_test_queuer(pid, receiver, count, max) when count < max do
spawn(fn() ->
case __MODULE__.checkout(pid, 5000, false) do
{:ok, w_pid} ->
:ok = GenServer.call(w_pid, :test)
send(receiver, :ok)
__MODULE__.checkin(pid, w_pid)
error ->
send(receiver, :failed)
:error
end
end)
feed_test_queuer(pid, receiver, count + 1, max)
end
def feed_test_queuer(_, _, _, _), do: :ok
def test_poolboy(max \\ @total_reqs) do
pid = Process.whereis(:worker)
time = :erlang.system_time(:millisecond)
receiver = spawn(__MODULE__, :keep_track, [time, 0, 0])
feed_test_poolboy(pid, receiver, 0, max)
feed_test_poolboy(pid, receiver, 0, max)
end
def feed_test_poolboy(pid, receiver, count, max) when count < max do
spawn(fn() ->
case :poolboy.checkout(pid, true, :infinity) do
:full ->
send(receiver, :failed)
:error
w_pid ->
:ok = GenServer.call(w_pid, :test)
send(receiver, :ok)
:poolboy.checkin(pid, w_pid)
end
end)
feed_test_poolboy(pid, receiver, count + 1, max)
end
def feed_test_poolboy(_, _, _, _), do: :ok
def keep_track(time, c, f) when (c + f) == @total_reqs do
time_now = :erlang.system_time(:millisecond)
IO.puts("Total ok: #{c} - Total failed: #{f}")
IO.inspect(((time_now - time) / 1000), label: "time")
end
def keep_track(time, count, failed) do
receive do
:ok -> keep_track(time, count + 1, failed)
:failed -> keep_track(time, count, failed + 1)
after
10_000 ->
:ok
end
end
In short, checkout a worker, call it for :ok
, send a message to the “tracker”.
The worker module is:
defmodule WorkerTest do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, nil, [])
end
def start_link(_) do
GenServer.start_link(__MODULE__, nil, [])
end
def init(_) do
{:ok, nil}
end
def handle_call(:test, _from, state) do
{:reply, :ok, state}
end
end
(it has two start_links, because my queuer is passing 0 args to it)
Can you see something wrong with the way I’m testing both implementations?