Packages
rsrc_pool_ex
1.0.3
The goal of ResourcePool (rsrc_pool_ex) Elixir library is reduce the overhead of creating new resources by reusing of the same resources among multiple processes.
Current section
Files
Jump to
Current section
Files
lib/resource_pool_srv.ex
defmodule PoolState do
@moduledoc false
defstruct active: [],
idle: [],
waiting: [],
max_active: 8,
max_idle: 8,
min_idle: 0,
test_on_borrow: false,
test_on_return: false,
fifo: false,
when_exhausted_action: :block, # :fail | :grow | :block
max_wait: :infinity, # integer() | :infinity,
max_idle_time: :infinity, # integer() | :infinity,
factory_module: nil,
resource_metadata: nil
end
defmodule ResourcePool.GenServer do
@moduledoc """
The module `ResourcePool.GenServer` implements resource pool functionality.
"""
use GenServer
@doc false
def child_spec(init_arg) do
[ResourcePool.GenServer, init_arg]
end
@doc """
Takes argument tuple of options, factory_module and resource_metadata.
"""
@impl true
def init({options, factory_module, resource_metadata}) do
max_active = Keyword.get(options, :max_active, 8)
state = %PoolState{
max_active: max_active,
max_idle: Keyword.get(options, :max_idle, max_active),
min_idle: Keyword.get(options, :min_idle, 0),
max_wait: Keyword.get(options, :max_wait, :infinity),
max_idle_time: Keyword.get(options, :max_idle_time, :infinity),
when_exhausted_action: Keyword.get(options, :when_exhausted_action, :block),
test_on_borrow: Keyword.get(options, :test_on_borrow, false),
test_on_return: Keyword.get(options, :test_on_return, false),
fifo: Keyword.get(options, :fifo, false),
factory_module: factory_module,
resource_metadata: resource_metadata
}
{:ok, state}
end
@doc """
* handle message `:borrow`
* handle message `{:ack_borrow, {:error, :pool_timeout}}`
"""
@impl true
def handle_call(:borrow, from, %PoolState{active: active, idle: idle} = state) do
num_active = length(active)
num_idle = length(idle)
process_borrow(from, state, num_active, num_idle)
end
def handle_call({:ack_borrow, {:error, :pool_timeout}}, {waiting_client, _}, %PoolState{active: active, waiting: waiting} = state) do
case List.keytake(active, {:tmp, waiting_client}, 1) do
nil -> {:reply, :ok, %PoolState{state | waiting: List.delete(waiting, waiting_client)}} # pure timeout
{{resource, _}, new_active} -> {:reply, :ok, %PoolState{state | active: new_active, idle: add_to_idle(resource, state)}}
end
end
def handle_call({:ack_borrow, resource}, {waiting_client, _}, %PoolState{active: active} = state) do
# io:format(user, " >>> resource_pool_srv:handle_call(ack_borrow, ..) from: ~p receive:~p active:~p~n", [Waiting_client, Receive, Active]),
{:reply, :ok, %PoolState{state | active: List.keyreplace(active, {:tmp, waiting_client}, 1, {resource, waiting_client})}}
end
def handle_call(:get_all_resources, _from, state) do
{:reply, Enum.map(state.active ++ state.idle, fn({r, _}) -> r end), state}
end
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
def handle_call(:get_number, _from, %PoolState{active: active, idle: idle} = state) do
{:reply, length(active) + length(idle), state}
end
def handle_call(:get_num_active, _from, %PoolState{active: active} = state) do
{:reply, length(active), state}
end
def handle_call(:get_num_idle, _from, %PoolState{idle: idle} = state) do
{:reply, length(idle), state}
end
def handle_call({:invalidate, resource}, {requester, _}, %PoolState{active: active, factory_module: factory_mod, resource_metadata: rsrc_MD} = state) do
case List.keyfind(active, resource, 0) do
{resource, owner} when requester === owner ->
factory_mod.destroy(rsrc_MD, resource)
{:reply, :ok, %PoolState{state | active: List.keydelete(active, resource, 0)}}
{_, _} -> {:reply, {:error, :not_owner}, state}
nil -> {:reply, {:error, :not_active}, state}
end
end
@impl true
def handle_cast({:return, resource, requester}, state) do
%PoolState{active: active, idle: idle, factory_module: factory_mod, resource_metadata: rsrc_MD, waiting: waiting} = state
resource_tuple = List.keyfind(active, resource, 0)
# :io.format(:user, "~n >>> resource_pool_srv.handle_cast(:return, ..): {~p, ~p} {rsrc, owner}:~p req-er:~p ~p~n", [length(active), length(idle), resource_tuple, requester, waiting])
case resource_tuple do
nil -> {:noreply, state}
{resource, owner} when owner == requester ->
case waiting do
[] ->
new_idle =
case ((not state.test_on_return) or factory_mod.validate(rsrc_MD, resource))
and ((state.max_idle < 0) or (length(idle) < state.max_idle)) do
true -> add_to_idle(resource, state)
false ->
factory_mod.destroy(rsrc_MD, resource)
idle
end
{:noreply, %PoolState{state | active: List.keydelete(active, resource, 0), idle: new_idle}};
_ ->
{others, [waiting_client]} = Enum.split(waiting, length(waiting) - 1)
case (not state.test_on_borrow) or factory_mod.validate(rsrc_MD, resource) do
true ->
factory_mod.passivate(rsrc_MD, resource)
factory_mod.activate({rsrc_MD, waiting_client}, resource)
send(waiting_client, {:ok, resource})
{:noreply, %PoolState{state | active: List.keyreplace(active, resource, 0, {resource, {:tmp, waiting_client}}), waiting: others}};
false ->
factory_mod.destroy(rsrc_MD, resource)
{:noreply, %PoolState{state | active: List.keydelete(active, resource, 0)}}
end
end
{_, _} -> {:noreply, state}
end
end
def handle_cast(:add, %PoolState{idle: idle, factory_module: factory_mod, resource_metadata: rsrc_MD} = state) do
case ((state.max_idle < 0) or (length(idle) < state.max_idle)) do
true ->
case factory_mod.create(rsrc_MD) do
{:ok, resource} ->
{:noreply, %PoolState{state | idle: add_to_idle(resource, state)}}
{:error, _err} ->
{:noreply, state}
end
false -> {:noreply, state}
end
end
def handle_cast({:remove, {resource, _}}, %PoolState{idle: idle, factory_module: factory_mod} = state) do
if length(idle) <= state.min_idle do
{:noreply, state}
else
case List.keytake(idle, resource, 0) do
nil ->
{:noreply, state}
{_, new_idle} ->
factory_mod.destroy(state.resource_metadata, resource)
{:noreply, %PoolState{state | idle: new_idle}}
end
end
end
def handle_cast(:clear, %PoolState{active: active, idle: idle, factory_module: factory_mod, resource_metadata: rsrc_MD} = state) do
for {rsrc, _} <- active, do: factory_mod.destroy(rsrc_MD, rsrc)
for {rsrc, pid} <- idle do
send(pid, :cancel)
factory_mod.destroy(rsrc_MD, rsrc)
end
{:noreply, %PoolState{state | active: [], idle: []}}
end
def handle_cast(:close, %PoolState{active: active, idle: idle, factory_module: factory_mod, resource_metadata: rsrc_MD} = state) do
for {rsrc, _} <- active, do: factory_mod.destroy(rsrc_MD, rsrc)
for {rsrc, pid} <- idle do
send(pid, :cancel)
factory_mod.destroy(rsrc_MD, rsrc)
end
{:stop, :normal, state}
end
defp add_to_idle(resource, %PoolState{idle: idle, factory_module: factory_mod, resource_metadata: rsrc_MD} = state) do
factory_mod.passivate(rsrc_MD, resource)
self = self()
pid = spawn_link(fn() ->
receive do
:cancel -> :ok
after state.max_idle_time ->
GenServer.cast(self, {:remove, {resource, self()}})
end
end)
[{resource, pid} | idle]
end
#
defp process_borrow({owner, _},
%PoolState{waiting: waiting, when_exhausted_action: action, max_active: max_active, } = state,
num_active, _num_idle)
when (max_active > 0)
and ((num_active >= max_active)
and (action != :grow))
do
case action do
:fail -> {:reply, {:error, :pool_exhausted}, state};
:block ->
case Enum.member?(waiting, owner) do
true -> {:reply, {:wait, state.max_wait}, state};
false -> {:reply, {:wait, state.max_wait}, %PoolState{state | waiting: [owner | waiting]}}
end
end
end
defp process_borrow({owner, _} = from, %PoolState{active: active, factory_module: factory_mod, min_idle: min_idle, resource_metadata: rsrc_MD} = state, _num_active, num_idle)
when num_idle <= min_idle
do
case factory_mod.create(rsrc_MD) do
{:ok, resource} when (num_idle == min_idle) ->
factory_mod.activate({rsrc_MD, owner}, resource)
{:reply, {:ok, resource}, %PoolState{state | active: [{resource, owner} | active]}}
{:ok, resource} ->
handle_call(:borrow, from, %PoolState{state | idle: add_to_idle(resource, state)})
{:error, err} ->
{:reply, {:error, err}, state}
end
end
defp process_borrow({owner, _} = from, %PoolState{active: active, idle: idle, factory_module: factory_mod, resource_metadata: rsrc_MD} = state, _num_active, num_idle) do
{resource, pid, new_idle} =
case state.fifo do
false ->
[{rsrc, p} | n_idle] = idle
{rsrc, p, n_idle}
true ->
{n_idle, [{rsrc, p}]} = Enum.split(idle, num_idle - 1) # lists:split(num_idle - 1, idle)
{rsrc, p, n_idle}
end
send(pid, :cancel)
case (not state.test_on_borrow) or factory_mod.validate(rsrc_MD, resource) do
true ->
factory_mod.activate({rsrc_MD, owner}, resource)
{:reply, {:ok, resource}, %PoolState{state | idle: new_idle, active: [{resource, owner} | active]}}
false ->
factory_mod.destroy(rsrc_MD, resource)
handle_call(:borrow, from, %PoolState{state | idle: new_idle})
end
end
end