The pooler application allows you to manage pools of OTP behaviors such as gen_servers, gen_fsms, or supervisors, and provide consumers with exclusive access to pool members using pooler:take_member.
pooler:take_member/0
and
pooler:return_member/2
. The pooler server will keep track of
which members are in use and which are free. There is no need
to call pooler:return_member
if the consumer is a short-lived
process; in this case, pooler will detect the consumer's normal exit
and reclaim the member.
The need for pooler arose while writing an Erlang-based application
that uses Riak for data storage. Riak's protocol buffer client is a
gen_server
process that initiates a connection to a Riak node. A
pool is needed to avoid spinning up a new client for each request in
the application. Reusing clients also has the benefit of keeping the
vector clocks smaller since each client ID corresponds to an entry in
the vector clock.
When using the Erlang protocol buffer client for Riak, one should avoid accessing a given client concurrently. This is because each client is associated with a unique client ID that corresponds to an element in an object's vector clock. Concurrent action from the same client ID defeats the vector clock. For some further explanation, see post 1 and post 2. Note that concurrent access to Riak's pb client is actual ok as long as you avoid updating the same key at the same time. So the pool needs to have checkout/checkin semantics that give consumers exclusive access to a client.
On top of that, in order to evenly load a Riak cluster and be able to continue in the face of Riak node failures, consumers should spread their requests across clients connected to each node. The client pool provides an easy way to load balance.
Pool configuration is specified in the pooler application's
environment. This can be provided in a config file using -config
or
set at startup using application:set_env(pooler, pools,
Pools)
. Here's an example config file that creates three pools of
Riak pb clients each talking to a different node in a local cluster:
% pooler.config % Start Erlang as: erl -config pooler % -*- mode: erlang -*- % pooler app config [ {pooler, [ {pools, [ [{name, "rc8081"}, {max_count, 5}, {init_count, 2}, {start_mfa, {riakc_pb_socket, start_link, ["localhost", 8081]}}], [{name, "rc8082"}, {max_count, 5}, {init_count, 2}, {start_mfa, {riakc_pb_socket, start_link, ["localhost", 8082]}}], [{name, "rc8083"}, {max_count, 5}, {init_count, 2}, {start_mfa, {riakc_pb_socket, start_link, ["localhost", 8083]}}] ]} ]} ].
Each pool has a unique name, an initial and maximum number of members,
and an {M, F, A}
describing how to start members of the pool. When
pooler starts, it will create members in each pool according to
init_count
.
Here's an example session:
application:start(pooler). P = pooler:take_member(), % use P pooler:return_member(P, ok).
Once started, the main interaction you will have with pooler is through
two functions, take_member/0
and return_member/2
.
Call pooler:take_member()
to obtain a member from a randomly
selected pool. When you are done with it, return it to the pool using
pooler:return_member(Pid, ok)
. If you encountered an error using
the member, you can pass fail
as the second argument. In this case,
pooler will permanently remove that member from the pool and start a
new member to replace it. If your process is short lived, you can
omit the call to return_member
. In this case, pooler will detect
the normal exit of the consumer and reclaim the member.