Packages

A compile-time builder that generates a concurrent pool of worker processes, batchers and schedulers for parallel task execution

Current section

Files

Jump to
poolder lib worker.ex
Raw

lib/worker.ex

defmodule Poolder.Worker do
defmacro __using__(opts) do
name = Keyword.fetch!(opts, :name)
pool_size = Keyword.get(opts, :pool_size, 1)
retry = Keyword.get(opts, :retry, count: 0, backoff: 0)
retries = Keyword.get(retry, :count)
backoff = Keyword.get(retry, :backoff, 0)
callbacks = Keyword.get(opts, :callback, [])
mode = Keyword.get(opts, :mode, :round_robin)
replies =
for {call, {mod, fun}} <- callbacks do
quote do
defp reply(unquote(call), args, state) do
:erlang.apply(unquote(mod), unquote(fun), args)
{:noreply, state}
end
end
end
quote bind_quoted: [
name: name,
pool_size: pool_size,
retries: retries,
backoff: backoff,
mode: mode,
replies: replies
] do
use GenServer
@pool name
@pool_size pool_size
@retries retries
@backoff backoff
@catcher @retries > 0
@supervisor __MODULE__.Supervisor
@mode mode
@max_index @pool_size - 1
@range 0..@max_index
@call_timeout 5000
def child_spec(id: worker_id) do
%{
id: {@pool, worker_id},
start: {__MODULE__, :start_link, [worker_id]},
type: :worker,
restart: :transient,
shutdown: 500
}
end
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_pool, [opts]},
type: :supervisor,
restart: :transient,
shutdown: 500
}
end
def start_pool(_opts) do
children =
[
{Registry, keys: :unique, name: @pool}
] ++
for i <- @range, do: {__MODULE__, id: i}
if @mode == :round_robin do
cref = :counters.new(1, [:write_concurrency])
:persistent_term.put({@pool, :counter}, cref)
end
case Supervisor.start_link(Poolder.Supervisor, children, name: @supervisor) do
{:ok, pid} ->
__MODULE__.handle_pool_ready(pid)
{:ok, pid}
error ->
error
end
end
defp via_tuple(id), do: {:via, Registry, {@pool, id}}
@compile {:inline, try_execute: 3, reply: 3}
@behaviour Poolder.Worker
def start_link(id) do
GenServer.start_link(__MODULE__, id, name: via_tuple(id))
end
@impl true
def init(id) do
:persistent_term.put({@pool, id}, self())
handle_init(id)
end
case @mode do
# Round-robin dispatch
:round_robin ->
# Reset the counter to 0
defp reset_counter do
cref = :persistent_term.get({@pool, :counter})
:counters.put(cref, 1, 0)
end
@max_number 1_000_000
def next_pid do
cref = :persistent_term.get({@pool, :counter})
:counters.add(cref, 1, 1)
number = :counters.get(cref, 1)
key = rem(number, @pool_size)
if number > @max_number, do: reset_counter()
:persistent_term.get({@pool, key})
end
# Random dispatch
:random ->
def next_pid do
number = :rand.uniform(@pool_size) - 1
:persistent_term.get({@pool, number})
end
# Monotonic time dispatch
:monotonic ->
def next_pid do
number = :erlang.monotonic_time() |> abs() |> rem(@pool_size)
:persistent_term.get({@pool, number})
end
# PHash dispatch
:phash ->
def next_pid(data) do
number = :erlang.phash2(data, @pool_size)
:persistent_term.get({@pool, number})
end
:broadcast ->
def next_pid, do: nil
_ ->
raise "Invalid pool mode: #{@mode} use :round_robin, :random, :monotonic, :broadcast or :phash"
end
def pid(id), do: :persistent_term.get({@pool, id}, nil)
def size, do: @pool_size
case @mode do
:phash ->
def cast(data) do
pid = next_pid(data)
GenServer.cast(pid, data)
end
def call(data, timeout \\ @call_timeout) do
try do
pid = next_pid(data)
{:ok, GenServer.call(pid, data, timeout)}
catch
:exit, {:timeout, _} -> {:error, :timeout}
:exit, reason -> {:error, reason}
end
end
:broadcast ->
def cast(data) do
for i <- @range do
pid = :persistent_term.get({@pool, i})
GenServer.cast(pid, data)
end
end
def call(data, timeout \\ @call_timeout) do
for i <- @range do
try do
pid = :persistent_term.get({@pool, i})
{:ok, GenServer.call(pid, data, timeout)}
catch
:exit, {:timeout, _} -> {:error, :timeout}
:exit, reason -> {:error, reason}
end
end
end
_ ->
def cast(data) do
pid = next_pid()
GenServer.cast(pid, data)
end
def call(data, timeout \\ @call_timeout) do
try do
pid = next_pid()
{:ok, GenServer.call(pid, data, timeout)}
catch
:exit, {:timeout, _} -> {:error, :timeout}
:exit, reason -> {:error, reason}
end
end
end
def cast(pid, data) do
GenServer.cast(pid, data)
end
@impl true
def handle_cast(data, state) do
try_execute(data, 1, state)
end
@impl true
def handle_call(data, _from, state) do
try_execute(data, 1, state)
end
@impl true
def handle_info({:retry_job, data, attempt}, state) do
try_execute(data, attempt, state)
end
if replies do
replies
end
defp reply(_call, message, state), do: {:reply, message, state}
defp try_execute(data, attempt, state) when attempt <= @retries do
try do
case handle_job(data, state) do
{:noreply, new_state} ->
{:noreply, new_state}
# Push a message
{call, args, new_state} ->
reply(call, args, new_state)
# Stop the pool
{:stop, new_state} ->
Supervisor.stop(@supervisor, :normal)
{:stop, :normal, new_state}
# Close the process
{:exit, new_state} ->
{:stop, :normal, new_state}
_ ->
{:noreply, state}
end
rescue
error ->
(@catcher and
case handle_error(data, attempt, error, state) do
{:retry, new_state} ->
try_execute(data, attempt + 1, new_state)
{:backoff, delay} ->
Process.send_after(self(), {:retry_job, data, attempt + 1}, delay)
{:noreply, state}
:halt ->
{:noreply, state}
_ ->
{:noreply, state}
end) || {:noreply, state}
end
end
defp try_execute(_data, _attempt, state), do: {:noreply, state}
# Callbacks
def handle_init(id), do: {:ok, %{id: id}}
def handle_job(_data, state), do: {:noreply, state}
if @backoff > 0 do
def handle_error(_data, _attempt, _error, state), do: {:backoff, @backoff}
else
def handle_error(_data, _attempt, _error, state), do: {:retry, state}
end
def handle_pool_ready(_pid), do: :ok
defoverridable handle_init: 1,
handle_job: 2,
handle_error: 4,
handle_pool_ready: 1
end
end
## Behaviour
@callback handle_init(id :: integer()) :: {:ok, state :: any()}
@callback handle_pool_ready(supervisor_pid :: pid()) :: any()
@callback handle_job(data :: any(), state :: any()) ::
{:noreply, state :: any()}
| {:push, any(), any(), state :: any()}
| {:reply, reply :: any(), state :: any()}
| {:stop, reason :: any(), state :: any()}
| {:exit, reason :: any(), state :: any()}
@callback handle_error(data :: any(), attempt :: integer(), error :: any(), state :: any()) ::
{:retry, state :: any()} | {:delay, integer()} | :halt
end