|
@@ -1,5 +1,5 @@
|
|
|
%% @author Seth Falcon <seth@userprimary.net>
|
|
|
-%% @copyright 2011-2012 Seth Falcon
|
|
|
+%% @copyright 2011-2013 Seth Falcon
|
|
|
%% @doc This is the main interface to the pooler application
|
|
|
%%
|
|
|
%% To integrate with your application, you probably want to call
|
|
@@ -10,21 +10,12 @@
|
|
|
%%
|
|
|
-module(pooler).
|
|
|
-behaviour(gen_server).
|
|
|
--define(SERVER, ?MODULE).
|
|
|
-
|
|
|
--define(DEFAULT_ADD_RETRY, 1).
|
|
|
--define(DEFAULT_CULL_INTERVAL, {0, min}).
|
|
|
--define(DEFAULT_MAX_AGE, {0, min}).
|
|
|
|
|
|
+-include("pooler.hrl").
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
|
|
--type member_info() :: {string(), free | pid(), {_, _, _}}.
|
|
|
--type free_member_info() :: {string(), free, {_, _, _}}.
|
|
|
--type time_unit() :: min | sec | ms | mu.
|
|
|
--type time_spec() :: {non_neg_integer(), time_unit()}.
|
|
|
|
|
|
%% type specs for pool metrics
|
|
|
--type metric_label() :: binary().
|
|
|
-type metric_value() :: 'unknown_pid' |
|
|
|
non_neg_integer() |
|
|
|
{'add_pids_failed', non_neg_integer(), non_neg_integer()} |
|
|
@@ -32,56 +23,22 @@
|
|
|
'error_no_members'.
|
|
|
-type metric_type() :: 'counter' | 'histogram' | 'history' | 'meter'.
|
|
|
|
|
|
--record(pool, {
|
|
|
- name :: string(),
|
|
|
- max_count = 100 :: non_neg_integer(),
|
|
|
- init_count = 10 :: non_neg_integer(),
|
|
|
- start_mfa :: {atom(), atom(), [term()]},
|
|
|
- free_pids = [] :: [pid()],
|
|
|
- in_use_count = 0 :: non_neg_integer(),
|
|
|
- free_count = 0 :: non_neg_integer(),
|
|
|
- %% The number times to attempt adding a pool member if the
|
|
|
- %% pool size is below max_count and there are no free
|
|
|
- %% members. After this many tries, error_no_members will be
|
|
|
- %% returned by a call to take_member. NOTE: this value
|
|
|
- %% should be >= 2 or else the pool will not grow on demand
|
|
|
- %% when max_count is larger than init_count.
|
|
|
- add_member_retry = ?DEFAULT_ADD_RETRY :: non_neg_integer(),
|
|
|
-
|
|
|
- %% The interval to schedule a cull message. Both
|
|
|
- %% 'cull_interval' and 'max_age' are specified using a
|
|
|
- %% `time_spec()' type.
|
|
|
- cull_interval = ?DEFAULT_CULL_INTERVAL :: time_spec(),
|
|
|
- %% The maximum age for members.
|
|
|
- max_age = ?DEFAULT_MAX_AGE :: time_spec()
|
|
|
- }).
|
|
|
-
|
|
|
--record(state, {
|
|
|
- npools :: non_neg_integer(),
|
|
|
- pools = dict:new() :: dict(),
|
|
|
- pool_sups = dict:new() :: dict(),
|
|
|
- all_members = dict:new() :: dict(),
|
|
|
- consumer_to_pid = dict:new() :: dict(),
|
|
|
- pool_selector :: array()
|
|
|
- }).
|
|
|
-
|
|
|
--define(gv(X, Y), proplists:get_value(X, Y)).
|
|
|
--define(gv(X, Y, D), proplists:get_value(X, Y, D)).
|
|
|
-
|
|
|
%% ------------------------------------------------------------------
|
|
|
%% API Function Exports
|
|
|
%% ------------------------------------------------------------------
|
|
|
|
|
|
--export([start/1,
|
|
|
+-export([accept_member/2,
|
|
|
start_link/1,
|
|
|
- stop/0,
|
|
|
- take_member/0,
|
|
|
take_member/1,
|
|
|
- return_member/1,
|
|
|
+ take_group_member/1,
|
|
|
+ return_group_member/2,
|
|
|
+ return_group_member/3,
|
|
|
return_member/2,
|
|
|
- % remove_pool/2,
|
|
|
- % add_pool/1,
|
|
|
- pool_stats/0]).
|
|
|
+ return_member/3,
|
|
|
+ pool_stats/1,
|
|
|
+ manual_start/0,
|
|
|
+ new_pool/1,
|
|
|
+ rm_pool/1]).
|
|
|
|
|
|
%% ------------------------------------------------------------------
|
|
|
%% gen_server Function Exports
|
|
@@ -103,143 +60,230 @@
|
|
|
%% API Function Definitions
|
|
|
%% ------------------------------------------------------------------
|
|
|
|
|
|
-start_link(Config) ->
|
|
|
- gen_server:start_link({local, ?SERVER}, ?MODULE, Config, []).
|
|
|
+start_link(#pool{name = Name} = Pool) ->
|
|
|
+ gen_server:start_link({local, Name}, ?MODULE, Pool, []).
|
|
|
|
|
|
-start(Config) ->
|
|
|
- gen_server:start_link({local, ?SERVER}, ?MODULE, Config, []).
|
|
|
+manual_start() ->
|
|
|
+ application:start(sasl),
|
|
|
+ application:start(pooler).
|
|
|
|
|
|
-stop() ->
|
|
|
- gen_server:call(?SERVER, stop).
|
|
|
-
|
|
|
-%% @doc Obtain exclusive access to a member from a randomly selected pool.
|
|
|
+%% @doc Start a new pool described by the proplist `PoolConfig'. The
|
|
|
+%% following keys are required in the proplist:
|
|
|
%%
|
|
|
-%% If there are no free members in the randomly selected pool, then a
|
|
|
-%% member will be returned from the pool with the most free members.
|
|
|
-%% If no free members are available, 'error_no_members' is returned.
|
|
|
+%% <dl>
|
|
|
+%% <dt>`name'</dt>
|
|
|
+%% <dd>An atom giving the name of the pool.</dd>
|
|
|
+%% <dt>`init_count'</dt>
|
|
|
+%% <dd>Number of members to add to the pool at start. When the pool is
|
|
|
+%% started, `init_count' members will be started in parallel.</dd>
|
|
|
+%% <dt>`max_count'</dt>
|
|
|
+%% <dd>Maximum number of members in the pool.</dd>
|
|
|
+%% <dt>`start_mfa'</dt>
|
|
|
+%% <dd>A tuple of the form `{Mod, Fun, Args}' describing how to start
|
|
|
+%% new pool members.</dd>
|
|
|
+%% </dl>
|
|
|
%%
|
|
|
--spec take_member() -> pid() | error_no_members.
|
|
|
-take_member() ->
|
|
|
- gen_server:call(?SERVER, take_member, infinity).
|
|
|
+%% In addition, you can specify any of the following optional
|
|
|
+%% configuration options:
|
|
|
+%%
|
|
|
+%% <dl>
|
|
|
+%% <dt>`group'</dt>
|
|
|
+%% <dd>An atom giving the name of the group this pool belongs
|
|
|
+%% to. Pools sharing a common `group' value can be accessed using
|
|
|
+%% {@link take_group_member/1} and {@link return_group_member/2}.</dd>
|
|
|
+%% <dt>`cull_interval'</dt>
|
|
|
+%% <dd>Time between checks for stale pool members. Specified as
|
|
|
+%% `{Time, Unit}' where `Time' is a non-negative integer and `Unit'
|
|
|
+%% is one of `min', `sec', `ms', or `mu'. The default value of `{0,
|
|
|
+%% min}' disables stale member checking. When `Time' is greater than
|
|
|
+%% zero, a message will be sent to the pool at the configured interval
|
|
|
+%% to trigger the removal of members that have not been accessed in
|
|
|
+%% `max_age' time units.</dd>
|
|
|
+%% <dt>`max_age'</dt>
|
|
|
+%% <dd>Members idle longer than `max_age' time units are removed from
|
|
|
+%% the pool when stale checking is enabled via
|
|
|
+%% `cull_interval'. Culling of idle members will never reduce the pool
|
|
|
+%% below `init_count'. The value is specified as `{Time, Unit}'. Note
|
|
|
+%% that timers are not set on individual pool members and may remain
|
|
|
+%% in the pool beyond the configured `max_age' value since members are
|
|
|
+%% only removed on the interval configured via `cull_interval'.</dd>
|
|
|
+%% </dl>
|
|
|
+new_pool(PoolConfig) ->
|
|
|
+ pooler_sup:new_pool(PoolConfig).
|
|
|
+
|
|
|
+%% @doc Terminate the named pool.
|
|
|
+rm_pool(PoolName) ->
|
|
|
+ pooler_sup:rm_pool(PoolName).
|
|
|
+
|
|
|
+%% @doc For INTERNAL use. Adds `MemberPid' to the pool.
|
|
|
+-spec accept_member(atom() | pid(), pid() | {noproc, _}) -> ok.
|
|
|
+accept_member(PoolName, MemberPid) ->
|
|
|
+ gen_server:call(PoolName, {accept_member, MemberPid}).
|
|
|
|
|
|
%% @doc Obtain exclusive access to a member from `PoolName'.
|
|
|
%%
|
|
|
%% If no free members are available, 'error_no_members' is returned.
|
|
|
%%
|
|
|
--spec take_member(string()) -> pid() | error_no_members | error_no_pool.
|
|
|
-take_member(PoolName) when is_list(PoolName) ->
|
|
|
- gen_server:call(?SERVER, {take_member, PoolName}, infinity).
|
|
|
+-spec take_member(atom() | pid()) -> pid() | error_no_members.
|
|
|
+take_member(PoolName) when is_atom(PoolName) orelse is_pid(PoolName) ->
|
|
|
+ gen_server:call(PoolName, take_member, infinity).
|
|
|
+
|
|
|
+%% @doc Take a member from a randomly selected member of the group
|
|
|
+%% `GroupName'. Returns `MemberPid' or `error_no_members'. If no
|
|
|
+%% members are available in the randomly chosen pool, all other pools
|
|
|
+%% in the group are tried in order.
|
|
|
+-spec take_group_member(atom()) -> pid() | error_no_members | {error_no_group, atom()}.
|
|
|
+take_group_member(GroupName) ->
|
|
|
+ case pg2:get_local_members(GroupName) of
|
|
|
+ {error, {no_such_group, GroupName}} ->
|
|
|
+ {error_no_group, GroupName};
|
|
|
+ [] ->
|
|
|
+ error_no_members;
|
|
|
+ Pools ->
|
|
|
+ %% Put a random member at the front of the list and then
|
|
|
+ %% return the first member you can walking the list.
|
|
|
+ {_, _, X} = erlang:now(),
|
|
|
+ Idx = (X rem length(Pools)) + 1,
|
|
|
+ {PoolPid, Rest} = extract_nth(Idx, Pools),
|
|
|
+ take_first_pool([PoolPid | Rest])
|
|
|
+ end.
|
|
|
+
|
|
|
+take_first_pool([PoolPid | Rest]) ->
|
|
|
+ case take_member(PoolPid) of
|
|
|
+ error_no_members ->
|
|
|
+ take_first_pool(Rest);
|
|
|
+ Member ->
|
|
|
+ ets:insert(?POOLER_GROUP_TABLE, {Member, PoolPid}),
|
|
|
+ Member
|
|
|
+ end;
|
|
|
+take_first_pool([]) ->
|
|
|
+ error_no_members.
|
|
|
+
|
|
|
+%% this helper function returns `{Nth_Elt, Rest}' where `Nth_Elt' is
|
|
|
+%% the nth element of `L' and `Rest' is `L -- [Nth_Elt]'.
|
|
|
+extract_nth(N, L) ->
|
|
|
+ extract_nth(N, L, []).
|
|
|
+
|
|
|
+extract_nth(1, [H | T], Acc) ->
|
|
|
+ {H, Acc ++ T};
|
|
|
+extract_nth(N, [H | T], Acc) ->
|
|
|
+ extract_nth(N - 1, T, [H | Acc]);
|
|
|
+extract_nth(_, [], _) ->
|
|
|
+ error(badarg).
|
|
|
+
|
|
|
+%% @doc Return a member that was taken from the group
|
|
|
+%% `GroupName'. This is a convenience function for
|
|
|
+%% `return_group_member/3' with `Status' of `ok'.
|
|
|
+-spec return_group_member(atom(), pid() | error_no_members) -> ok.
|
|
|
+return_group_member(GroupName, MemberPid) ->
|
|
|
+ return_group_member(GroupName, MemberPid, ok).
|
|
|
+
|
|
|
+%% @doc Return a member that was taken from the group `GroupName'. If
|
|
|
+%% `Status' is `ok' the member is returned to the pool from which is
|
|
|
+%% came. If `Status' is `fail' the member will be terminated and a new
|
|
|
+%% member added to the appropriate pool.
|
|
|
+-spec return_group_member(atom(), pid() | error_no_members, ok | fail) -> ok.
|
|
|
+return_group_member(_, error_no_members, _) ->
|
|
|
+ ok;
|
|
|
+return_group_member(_GroupName, MemberPid, Status) ->
|
|
|
+ case ets:lookup(?POOLER_GROUP_TABLE, MemberPid) of
|
|
|
+ [{MemberPid, PoolPid}] ->
|
|
|
+ return_member(PoolPid, MemberPid, Status);
|
|
|
+ [] ->
|
|
|
+ ok
|
|
|
+ end.
|
|
|
|
|
|
%% @doc Return a member to the pool so it can be reused.
|
|
|
%%
|
|
|
%% If `Status' is 'ok', the member is returned to the pool. If
|
|
|
%% `Status' is 'fail', the member is destroyed and a new member is
|
|
|
%% added to the pool in its place.
|
|
|
--spec return_member(pid() | error_no_members, ok | fail) -> ok.
|
|
|
-return_member(Pid, Status) when is_pid(Pid) andalso
|
|
|
- (Status =:= ok orelse Status =:= fail) ->
|
|
|
- gen_server:call(?SERVER, {return_member, Pid, Status}, infinity),
|
|
|
+-spec return_member(atom() | pid(), pid() | error_no_members, ok | fail) -> ok.
|
|
|
+return_member(PoolName, Pid, Status) when is_pid(Pid) andalso
|
|
|
+ (is_atom(PoolName) orelse
|
|
|
+ is_pid(PoolName)) andalso
|
|
|
+ (Status =:= ok orelse
|
|
|
+ Status =:= fail) ->
|
|
|
+ gen_server:call(PoolName, {return_member, Pid, Status}, infinity),
|
|
|
ok;
|
|
|
-return_member(error_no_members, _) ->
|
|
|
+return_member(_, error_no_members, _) ->
|
|
|
ok.
|
|
|
|
|
|
%% @doc Return a member to the pool so it can be reused.
|
|
|
%%
|
|
|
--spec return_member(pid() | error_no_members) -> ok.
|
|
|
-return_member(Pid) when is_pid(Pid) ->
|
|
|
- gen_server:call(?SERVER, {return_member, Pid, ok}, infinity),
|
|
|
+-spec return_member(atom() | pid(), pid() | error_no_members) -> ok.
|
|
|
+return_member(PoolName, Pid) when is_pid(Pid) andalso
|
|
|
+ (is_atom(PoolName) orelse is_pid(PoolName)) ->
|
|
|
+ gen_server:call(PoolName, {return_member, Pid, ok}, infinity),
|
|
|
ok;
|
|
|
-return_member(error_no_members) ->
|
|
|
+return_member(_, error_no_members) ->
|
|
|
ok.
|
|
|
|
|
|
-% TODO:
|
|
|
-% remove_pool(Name, How) when How == graceful; How == immediate ->
|
|
|
-% gen_server:call(?SERVER, {remove_pool, Name, How}).
|
|
|
-
|
|
|
-% TODO:
|
|
|
-% add_pool(Pool) ->
|
|
|
-% gen_server:call(?SERVER, {add_pool, Pool}).
|
|
|
-
|
|
|
%% @doc Obtain runtime state info for all pools.
|
|
|
%%
|
|
|
%% Format of the return value is subject to change.
|
|
|
--spec pool_stats() -> [tuple()].
|
|
|
-pool_stats() ->
|
|
|
- gen_server:call(?SERVER, pool_stats).
|
|
|
+-spec pool_stats(atom() | pid()) -> [tuple()].
|
|
|
+pool_stats(PoolName) ->
|
|
|
+ gen_server:call(PoolName, pool_stats).
|
|
|
|
|
|
%% ------------------------------------------------------------------
|
|
|
%% gen_server Function Definitions
|
|
|
%% ------------------------------------------------------------------
|
|
|
|
|
|
--spec init([any()]) -> {'ok', #state{npools::'undefined' | non_neg_integer(),
|
|
|
- pools::dict(),
|
|
|
- pool_sups::dict(),
|
|
|
- all_members::dict(),
|
|
|
- consumer_to_pid::dict(),
|
|
|
- pool_selector::'undefined' | array()}}.
|
|
|
-init(Config) ->
|
|
|
- process_flag(trap_exit, true),
|
|
|
- PoolRecs = [ props_to_pool(P) || P <- ?gv(pools, Config) ],
|
|
|
- Pools = [ {Pool#pool.name, Pool} || Pool <- PoolRecs ],
|
|
|
- PoolSups = [ begin
|
|
|
- {ok, SupPid} = supervisor:start_child(pooler_pool_sup, [MFA]),
|
|
|
- {Name, SupPid}
|
|
|
- end || #pool{name = Name, start_mfa = MFA} <- PoolRecs ],
|
|
|
- State0 = #state{npools = length(Pools),
|
|
|
- pools = dict:from_list(Pools),
|
|
|
- pool_sups = dict:from_list(PoolSups),
|
|
|
- pool_selector = array:from_list([PN || {PN, _} <- Pools])
|
|
|
- },
|
|
|
-
|
|
|
- lists:foldl(fun(#pool{name = PName, init_count = N}, {ok, AccState}) ->
|
|
|
- AccState1 = cull_members(PName, AccState),
|
|
|
- add_pids(PName, N, AccState1)
|
|
|
- end, {ok, State0}, PoolRecs).
|
|
|
-
|
|
|
-handle_call(take_member, {CPid, _Tag},
|
|
|
- #state{pool_selector = PS, npools = NP} = State) ->
|
|
|
- % attempt to return a member from a randomly selected pool. If
|
|
|
- % that pool has no members, find the pool with most free members
|
|
|
- % and return a member from there.
|
|
|
- PoolName = array:get(crypto:rand_uniform(0, NP), PS),
|
|
|
- case take_member(PoolName, CPid, State) of
|
|
|
- {error_no_members, NewState} ->
|
|
|
- case max_free_pool(State#state.pools) of
|
|
|
- error_no_members ->
|
|
|
- {reply, error_no_members, NewState};
|
|
|
- MaxFreePoolName ->
|
|
|
- {NewPid, State2} = take_member(MaxFreePoolName, CPid,
|
|
|
- NewState),
|
|
|
- {reply, NewPid, State2}
|
|
|
- end;
|
|
|
- {NewPid, NewState} ->
|
|
|
- {reply, NewPid, NewState}
|
|
|
- end;
|
|
|
-handle_call({take_member, PoolName}, {CPid, _Tag}, #state{} = State) ->
|
|
|
- {Member, NewState} = take_member(PoolName, CPid, State),
|
|
|
- {reply, Member, NewState};
|
|
|
-handle_call({return_member, Pid, Status}, {_CPid, _Tag}, State) ->
|
|
|
- {reply, ok, do_return_member(Pid, Status, State)};
|
|
|
-handle_call(stop, _From, State) ->
|
|
|
- {stop, normal, stop_ok, State};
|
|
|
-handle_call(pool_stats, _From, State) ->
|
|
|
- {reply, dict:to_list(State#state.all_members), State};
|
|
|
-handle_call(_Request, _From, State) ->
|
|
|
- {noreply, State}.
|
|
|
+-spec init(#pool{}) -> {'ok', #pool{}, 0}.
|
|
|
+init(#pool{}=Pool) ->
|
|
|
+ #pool{init_count = N} = Pool,
|
|
|
+ MemberSup = pooler_pool_sup:member_sup_name(Pool),
|
|
|
+ Pool1 = set_member_sup(Pool, MemberSup),
|
|
|
+ %% This schedules the next cull when the pool is configured for
|
|
|
+ %% such and is otherwise a no-op.
|
|
|
+ Pool2 = cull_members_from_pool(Pool1),
|
|
|
+ {ok, NewPool} = init_members_sync(N, Pool2),
|
|
|
+ %% trigger an immediate timeout, handled by handle_info to allow
|
|
|
+ %% us to register with pg2. We use the timeout mechanism to ensure
|
|
|
+ %% that a server is added to a group only when it is ready to
|
|
|
+ %% process messages.
|
|
|
+ {ok, NewPool, 0}.
|
|
|
+
|
|
|
+set_member_sup(#pool{} = Pool, MemberSup) ->
|
|
|
+ Pool#pool{member_sup = MemberSup}.
|
|
|
+
|
|
|
+handle_call(take_member, {CPid, _Tag}, #pool{} = Pool) ->
|
|
|
+ {Member, NewPool} = take_member_from_pool(Pool, CPid),
|
|
|
+ {reply, Member, NewPool};
|
|
|
+handle_call({return_member, Pid, Status}, {_CPid, _Tag}, Pool) ->
|
|
|
+ {reply, ok, do_return_member(Pid, Status, Pool)};
|
|
|
+handle_call({accept_member, Pid}, _From, Pool) ->
|
|
|
+ {reply, ok, do_accept_member(Pid, Pool)};
|
|
|
+handle_call(stop, _From, Pool) ->
|
|
|
+ {stop, normal, stop_ok, Pool};
|
|
|
+handle_call(pool_stats, _From, Pool) ->
|
|
|
+ {reply, dict:to_list(Pool#pool.all_members), Pool};
|
|
|
+handle_call(dump_pool, _From, Pool) ->
|
|
|
+ {reply, Pool, Pool};
|
|
|
+handle_call(_Request, _From, Pool) ->
|
|
|
+ {noreply, Pool}.
|
|
|
|
|
|
-spec handle_cast(_,_) -> {'noreply', _}.
|
|
|
-handle_cast(_Msg, State) ->
|
|
|
- {noreply, State}.
|
|
|
+handle_cast(_Msg, Pool) ->
|
|
|
+ {noreply, Pool}.
|
|
|
|
|
|
-spec handle_info(_, _) -> {'noreply', _}.
|
|
|
-handle_info({'EXIT', Pid, Reason}, State) ->
|
|
|
+handle_info(timeout, #pool{group = undefined} = Pool) ->
|
|
|
+ %% ignore
|
|
|
+ {noreply, Pool};
|
|
|
+handle_info(timeout, #pool{group = Group} = Pool) ->
|
|
|
+ ok = pg2:create(Group),
|
|
|
+ ok = pg2:join(Group, self()),
|
|
|
+ {noreply, Pool};
|
|
|
+handle_info({'DOWN', MRef, process, Pid, Reason}, State) ->
|
|
|
State1 =
|
|
|
- case dict:find(Pid, State#state.all_members) of
|
|
|
+ case dict:find(Pid, State#pool.all_members) of
|
|
|
{ok, {_PoolName, _ConsumerPid, _Time}} ->
|
|
|
do_return_member(Pid, fail, State);
|
|
|
error ->
|
|
|
- case dict:find(Pid, State#state.consumer_to_pid) of
|
|
|
- {ok, Pids} ->
|
|
|
+ case dict:find(Pid, State#pool.consumer_to_pid) of
|
|
|
+ {ok, {MRef, Pids}} ->
|
|
|
IsOk = case Reason of
|
|
|
normal -> ok;
|
|
|
_Crash -> fail
|
|
@@ -252,8 +296,8 @@ handle_info({'EXIT', Pid, Reason}, State) ->
|
|
|
end
|
|
|
end,
|
|
|
{noreply, State1};
|
|
|
-handle_info({cull_pool, PoolName}, State) ->
|
|
|
- {noreply, cull_members(PoolName, State)};
|
|
|
+handle_info(cull_pool, Pool) ->
|
|
|
+ {noreply, cull_members_from_pool(Pool)};
|
|
|
handle_info(_Info, State) ->
|
|
|
{noreply, State}.
|
|
|
|
|
@@ -269,143 +313,171 @@ code_change(_OldVsn, State, _Extra) ->
|
|
|
%% Internal Function Definitions
|
|
|
%% ------------------------------------------------------------------
|
|
|
|
|
|
--spec props_to_pool([{atom(), term()}]) -> #pool{}.
|
|
|
-props_to_pool(P) ->
|
|
|
- #pool{ name = ?gv(name, P),
|
|
|
- max_count = ?gv(max_count, P),
|
|
|
- init_count = ?gv(init_count, P),
|
|
|
- start_mfa = ?gv(start_mfa, P),
|
|
|
- add_member_retry = ?gv(add_member_retry, P, ?DEFAULT_ADD_RETRY),
|
|
|
- cull_interval = ?gv(cull_interval, P, ?DEFAULT_CULL_INTERVAL),
|
|
|
- max_age = ?gv(max_age, P, ?DEFAULT_MAX_AGE)}.
|
|
|
-
|
|
|
-% FIXME: creation of new pids should probably happen
|
|
|
-% in a spawned process to avoid tying up the loop.
|
|
|
--spec add_pids(error | string(), non_neg_integer(), #state{}) ->
|
|
|
- {bad_pool_name | max_count_reached | ok, #state{}}.
|
|
|
-add_pids(error, _N, State) ->
|
|
|
- {bad_pool_name, State};
|
|
|
-add_pids(PoolName, N, State) ->
|
|
|
- #state{pools = Pools, all_members = AllMembers} = State,
|
|
|
- Pool = fetch_pool(PoolName, Pools),
|
|
|
- #pool{max_count = Max, free_pids = Free,
|
|
|
- in_use_count = NumInUse, free_count = NumFree} = Pool,
|
|
|
- Total = NumFree + NumInUse,
|
|
|
- case Total + N =< Max of
|
|
|
+do_accept_member({Ref, Pid},
|
|
|
+ #pool{
|
|
|
+ all_members = AllMembers,
|
|
|
+ free_pids = Free,
|
|
|
+ free_count = NumFree,
|
|
|
+ starting_members = StartingMembers0
|
|
|
+ } = Pool) when is_pid(Pid) ->
|
|
|
+ %% make sure we don't accept a timedout member
|
|
|
+ StartingMembers = remove_stale_starting_members(Pool, StartingMembers0,
|
|
|
+ ?DEFAULT_MEMBER_START_TIMEOUT),
|
|
|
+ case lists:keymember(Ref, 1, StartingMembers) of
|
|
|
+ false ->
|
|
|
+ %% a pid we didn't ask to start, ignore it.
|
|
|
+ %% should we log it?
|
|
|
+ Pool;
|
|
|
true ->
|
|
|
- PoolSup = dict:fetch(PoolName, State#state.pool_sups),
|
|
|
- {AllMembers1, NewPids} = start_n_pids(N, PoolName, PoolSup,
|
|
|
- AllMembers),
|
|
|
- %% start_n_pids may return fewer than N if errors were
|
|
|
- %% encountered.
|
|
|
- NewPidCount = length(NewPids),
|
|
|
- case NewPidCount =:= N of
|
|
|
- true -> ok;
|
|
|
- false ->
|
|
|
- error_logger:error_msg("tried to add ~B members, only added ~B~n",
|
|
|
- [N, NewPidCount]),
|
|
|
- send_metric(<<"pooler.events">>,
|
|
|
- {add_pids_failed, N, NewPidCount}, history)
|
|
|
- end,
|
|
|
- Pool1 = Pool#pool{free_pids = Free ++ NewPids,
|
|
|
- free_count = length(Free) + NewPidCount},
|
|
|
- {ok, State#state{pools = store_pool(PoolName, Pool1, Pools),
|
|
|
- all_members = AllMembers1}};
|
|
|
+ StartingMembers1 = lists:keydelete(Ref, 1, StartingMembers),
|
|
|
+ MRef = erlang:monitor(process, Pid),
|
|
|
+ Entry = {MRef, free, os:timestamp()},
|
|
|
+ AllMembers1 = store_all_members(Pid, Entry, AllMembers),
|
|
|
+ Pool#pool{free_pids = Free ++ [Pid],
|
|
|
+ free_count = NumFree + 1,
|
|
|
+ all_members = AllMembers1,
|
|
|
+ starting_members = StartingMembers1}
|
|
|
+ end;
|
|
|
+do_accept_member({Ref, _Reason}, #pool{starting_members = StartingMembers0} = Pool) ->
|
|
|
+ %% member start failed, remove in-flight ref and carry on.
|
|
|
+ StartingMembers = remove_stale_starting_members(Pool, StartingMembers0,
|
|
|
+ ?DEFAULT_MEMBER_START_TIMEOUT),
|
|
|
+ StartingMembers1 = lists:keydelete(Ref, 1, StartingMembers),
|
|
|
+ Pool#pool{starting_members = StartingMembers1}.
|
|
|
+
|
|
|
+
|
|
|
+-spec remove_stale_starting_members(#pool{}, [{reference(), erlang:timestamp()}],
|
|
|
+ time_spec()) -> [{reference(), erlang:timestamp()}].
|
|
|
+remove_stale_starting_members(Pool, StartingMembers, MaxAge) ->
|
|
|
+ Now = os:timestamp(),
|
|
|
+ MaxAgeSecs = time_as_secs(MaxAge),
|
|
|
+ lists:filter(fun(SM) ->
|
|
|
+ starting_member_not_stale(Pool, Now, SM, MaxAgeSecs)
|
|
|
+ end, StartingMembers).
|
|
|
+
|
|
|
+starting_member_not_stale(Pool, Now, {_Ref, StartTime}, MaxAgeSecs) ->
|
|
|
+ case secs_between(StartTime, Now) < MaxAgeSecs of
|
|
|
+ true ->
|
|
|
+ true;
|
|
|
false ->
|
|
|
- {max_count_reached, State}
|
|
|
+ error_logger:error_msg("pool '~s': starting member timeout", [Pool#pool.name]),
|
|
|
+ send_metric(Pool, starting_member_timeout, {inc, 1}, counter),
|
|
|
+ false
|
|
|
+ end.
|
|
|
+
|
|
|
+init_members_sync(N, #pool{name = PoolName} = Pool) ->
|
|
|
+ Self = self(),
|
|
|
+ StartTime = os:timestamp(),
|
|
|
+ StartRefs = [ {pooler_starter:start_member(Pool, Self), StartTime}
|
|
|
+ || _I <- lists:seq(1, N) ],
|
|
|
+ Pool1 = Pool#pool{starting_members = StartRefs},
|
|
|
+ case collect_init_members(Pool1) of
|
|
|
+ timeout ->
|
|
|
+ error_logger:error_msg("pool '~s': exceeded timeout waiting for ~B members",
|
|
|
+ [PoolName, Pool1#pool.init_count]),
|
|
|
+ error({timeout, "unable to start members"});
|
|
|
+ #pool{} = Pool2 ->
|
|
|
+ {ok, Pool2}
|
|
|
+ end.
|
|
|
+
|
|
|
+collect_init_members(#pool{starting_members = []} = Pool) ->
|
|
|
+ Pool;
|
|
|
+collect_init_members(#pool{} = Pool) ->
|
|
|
+ Timeout = time_as_millis(?DEFAULT_MEMBER_START_TIMEOUT),
|
|
|
+ receive
|
|
|
+ {accept_member, {Ref, Member}} ->
|
|
|
+ collect_init_members(do_accept_member({Ref, Member}, Pool))
|
|
|
+ after
|
|
|
+ Timeout ->
|
|
|
+ timeout
|
|
|
end.
|
|
|
|
|
|
--spec take_member(string(), {pid(), _}, #state{}) ->
|
|
|
- {error_no_pool | error_no_members | pid(), #state{}}.
|
|
|
-take_member(PoolName, From, #state{pools = Pools} = State) ->
|
|
|
- Pool = fetch_pool(PoolName, Pools),
|
|
|
- take_member_from_pool(Pool, From, State, pool_add_retries(Pool)).
|
|
|
-
|
|
|
--spec take_member_from_pool(error_no_pool | #pool{}, {pid(), term()}, #state{},
|
|
|
- non_neg_integer()) ->
|
|
|
- {error_no_pool | error_no_members | pid(), #state{}}.
|
|
|
-take_member_from_pool(error_no_pool, _From, State, _) ->
|
|
|
- {error_no_pool, State};
|
|
|
-take_member_from_pool(#pool{name = PoolName,
|
|
|
+-spec take_member_from_pool(#pool{}, {pid(), term()}) ->
|
|
|
+ {error_no_members | pid(), #pool{}}.
|
|
|
+take_member_from_pool(#pool{init_count = InitCount,
|
|
|
max_count = Max,
|
|
|
free_pids = Free,
|
|
|
in_use_count = NumInUse,
|
|
|
- free_count = NumFree} = Pool,
|
|
|
- From,
|
|
|
- #state{pools = Pools, consumer_to_pid = CPMap} = State,
|
|
|
- Retries) ->
|
|
|
- send_metric(pool_metric(PoolName, take_rate), 1, meter),
|
|
|
+ free_count = NumFree,
|
|
|
+ consumer_to_pid = CPMap,
|
|
|
+ starting_members = StartingMembers0} = Pool,
|
|
|
+ From) ->
|
|
|
+ send_metric(Pool, take_rate, 1, meter),
|
|
|
+ StartingMembers = remove_stale_starting_members(Pool, StartingMembers0,
|
|
|
+ ?DEFAULT_MEMBER_START_TIMEOUT),
|
|
|
+ NumCanAdd = Max - (NumInUse + NumFree + length(StartingMembers)),
|
|
|
case Free of
|
|
|
- [] when NumInUse =:= Max ->
|
|
|
- send_metric(<<"pooler.error_no_members_count">>, {inc, 1}, counter),
|
|
|
- send_metric(<<"pooler.events">>, error_no_members, history),
|
|
|
- {error_no_members, State};
|
|
|
- [] when NumInUse < Max andalso Retries > 0 ->
|
|
|
- case add_pids(PoolName, 1, State) of
|
|
|
- {ok, State1} ->
|
|
|
- %% add_pids may have updated our pool
|
|
|
- Pool1 = fetch_pool(PoolName, State1#state.pools),
|
|
|
- take_member_from_pool(Pool1, From, State1, Retries - 1);
|
|
|
- {max_count_reached, _} ->
|
|
|
- send_metric(<<"pooler.error_no_members_count">>, {inc, 1}, counter),
|
|
|
- send_metric(<<"pooler.events">>, error_no_members, history),
|
|
|
- {error_no_members, State}
|
|
|
- end;
|
|
|
- [] when Retries =:= 0 ->
|
|
|
- %% max retries reached
|
|
|
- send_metric(<<"pooler.error_no_members_count">>, {inc, 1}, counter),
|
|
|
- {error_no_members, State};
|
|
|
+ [] when NumCanAdd =< 0 ->
|
|
|
+ send_metric(Pool, error_no_members_count, {inc, 1}, counter),
|
|
|
+ send_metric(Pool, events, error_no_members, history),
|
|
|
+ {error_no_members, Pool};
|
|
|
+ [] when NumCanAdd > 0 ->
|
|
|
+ %% Limit concurrently starting members to init_count. Add
|
|
|
+ %% up to init_count members. Starting members here means
|
|
|
+ %% we always return an error_no_members for a take request
|
|
|
+ %% when all members are in-use. By adding a batch of new
|
|
|
+ %% members, the pool should reach a steady state with
|
|
|
+ %% unused members culled over time (if scheduled cull is
|
|
|
+ %% enabled).
|
|
|
+ NumToAdd = min(InitCount - length(StartingMembers), NumCanAdd),
|
|
|
+ Pool1 = add_members_async(NumToAdd, Pool),
|
|
|
+ send_metric(Pool, error_no_members_count, {inc, 1}, counter),
|
|
|
+ send_metric(Pool, events, error_no_members, history),
|
|
|
+ {error_no_members, Pool1};
|
|
|
[Pid|Rest] ->
|
|
|
- erlang:link(From),
|
|
|
Pool1 = Pool#pool{free_pids = Rest, in_use_count = NumInUse + 1,
|
|
|
free_count = NumFree - 1},
|
|
|
- send_metric(pool_metric(PoolName, in_use_count), Pool1#pool.in_use_count, histogram),
|
|
|
- send_metric(pool_metric(PoolName, free_count), Pool1#pool.free_count, histogram),
|
|
|
- {Pid, State#state{
|
|
|
- pools = store_pool(PoolName, Pool1, Pools),
|
|
|
+ send_metric(Pool, in_use_count, Pool1#pool.in_use_count, histogram),
|
|
|
+ send_metric(Pool, free_count, Pool1#pool.free_count, histogram),
|
|
|
+ {Pid, Pool1#pool{
|
|
|
consumer_to_pid = add_member_to_consumer(Pid, From, CPMap),
|
|
|
all_members = set_cpid_for_member(Pid, From,
|
|
|
- State#state.all_members)
|
|
|
+ Pool1#pool.all_members)
|
|
|
}}
|
|
|
end.
|
|
|
|
|
|
--spec do_return_member(pid(), ok | fail, #state{}) -> #state{}.
|
|
|
-do_return_member(Pid, ok, #state{all_members = AllMembers} = State) ->
|
|
|
+%% @doc Add `Count' members to `Pool' asynchronously. Returns updated
|
|
|
+%% `Pool' record with starting member refs added to field
|
|
|
+%% `starting_members'.
|
|
|
+add_members_async(Count, #pool{starting_members = StartingMembers} = Pool) ->
|
|
|
+ StartTime = os:timestamp(),
|
|
|
+ StartRefs = [ {pooler_starter:start_member(Pool), StartTime}
|
|
|
+ || _I <- lists:seq(1, Count) ],
|
|
|
+ Pool#pool{starting_members = StartRefs ++ StartingMembers}.
|
|
|
+
|
|
|
+-spec do_return_member(pid(), ok | fail, #pool{}) -> #pool{}.
|
|
|
+do_return_member(Pid, ok, #pool{all_members = AllMembers} = Pool) ->
|
|
|
+ clean_group_table(Pid, Pool),
|
|
|
case dict:find(Pid, AllMembers) of
|
|
|
- {ok, {PoolName, CPid, _}} ->
|
|
|
- Pool = fetch_pool(PoolName, State#state.pools),
|
|
|
+ {ok, {MRef, CPid, _}} ->
|
|
|
#pool{free_pids = Free, in_use_count = NumInUse,
|
|
|
free_count = NumFree} = Pool,
|
|
|
Pool1 = Pool#pool{free_pids = [Pid | Free], in_use_count = NumInUse - 1,
|
|
|
free_count = NumFree + 1},
|
|
|
- Entry = {PoolName, free, os:timestamp()},
|
|
|
- State#state{pools = store_pool(PoolName, Pool1, State#state.pools),
|
|
|
- all_members = store_all_members(Pid, Entry, AllMembers),
|
|
|
- consumer_to_pid = cpmap_remove(Pid, CPid,
|
|
|
- State#state.consumer_to_pid)};
|
|
|
+ Entry = {MRef, free, os:timestamp()},
|
|
|
+ Pool1#pool{all_members = store_all_members(Pid, Entry, AllMembers),
|
|
|
+ consumer_to_pid = cpmap_remove(Pid, CPid,
|
|
|
+ Pool1#pool.consumer_to_pid)};
|
|
|
error ->
|
|
|
- State
|
|
|
+ Pool
|
|
|
end;
|
|
|
-do_return_member(Pid, fail, #state{all_members = AllMembers} = State) ->
|
|
|
+do_return_member(Pid, fail, #pool{all_members = AllMembers} = Pool) ->
|
|
|
% for the fail case, perhaps the member crashed and was alerady
|
|
|
% removed, so use find instead of fetch and ignore missing.
|
|
|
+ clean_group_table(Pid, Pool),
|
|
|
case dict:find(Pid, AllMembers) of
|
|
|
- {ok, {PoolName, _, _}} ->
|
|
|
- State1 = remove_pid(Pid, State),
|
|
|
- case add_pids(PoolName, 1, State1) of
|
|
|
- {Status, State2} when Status =:= ok;
|
|
|
- Status =:= max_count_reached ->
|
|
|
- State2;
|
|
|
- {Status, _} ->
|
|
|
- erlang:error({error, "unexpected return from add_pid",
|
|
|
- Status, erlang:get_stacktrace()}),
|
|
|
- send_metric(<<"pooler.events">>, bad_return_from_add_pid,
|
|
|
- history)
|
|
|
- end;
|
|
|
+ {ok, {_MRef, _, _}} ->
|
|
|
+ Pool1 = remove_pid(Pid, Pool),
|
|
|
+ add_members_async(1, Pool1);
|
|
|
error ->
|
|
|
- State
|
|
|
+ Pool
|
|
|
end.
|
|
|
|
|
|
+clean_group_table(_MemberPid, #pool{group = undefined}) ->
|
|
|
+ ok;
|
|
|
+clean_group_table(MemberPid, #pool{group = _GroupName}) ->
|
|
|
+ ets:delete(?POOLER_GROUP_TABLE, MemberPid).
|
|
|
+
|
|
|
% @doc Remove `Pid' from the pid list associated with `CPid' in the
|
|
|
% consumer to member map given by `CPMap'.
|
|
|
%
|
|
@@ -417,13 +489,14 @@ cpmap_remove(_Pid, free, CPMap) ->
|
|
|
CPMap;
|
|
|
cpmap_remove(Pid, CPid, CPMap) ->
|
|
|
case dict:find(CPid, CPMap) of
|
|
|
- {ok, Pids0} ->
|
|
|
- unlink(CPid), % FIXME: flush msg queue here?
|
|
|
+ {ok, {MRef, Pids0}} ->
|
|
|
Pids1 = lists:delete(Pid, Pids0),
|
|
|
case Pids1 of
|
|
|
[_H|_T] ->
|
|
|
- dict:store(CPid, Pids1, CPMap);
|
|
|
+ dict:store(CPid, {MRef, Pids1}, CPMap);
|
|
|
[] ->
|
|
|
+ %% no more members for this consumer
|
|
|
+ erlang:demonitor(MRef),
|
|
|
dict:erase(CPid, CPMap)
|
|
|
end;
|
|
|
error ->
|
|
@@ -436,142 +509,89 @@ cpmap_remove(Pid, CPid, CPMap) ->
|
|
|
% Handles in-use and free members. Logs an error if the pid is not
|
|
|
% tracked in state.all_members.
|
|
|
%
|
|
|
--spec remove_pid(pid(), #state{}) -> #state{}.
|
|
|
-remove_pid(Pid, State) ->
|
|
|
- #state{all_members = AllMembers, pools = Pools,
|
|
|
- consumer_to_pid = CPMap} = State,
|
|
|
+-spec remove_pid(pid(), #pool{}) -> #pool{}.
|
|
|
+remove_pid(Pid, Pool) ->
|
|
|
+ #pool{name = PoolName,
|
|
|
+ all_members = AllMembers,
|
|
|
+ consumer_to_pid = CPMap} = Pool,
|
|
|
case dict:find(Pid, AllMembers) of
|
|
|
- {ok, {PoolName, free, _Time}} ->
|
|
|
+ {ok, {MRef, free, _Time}} ->
|
|
|
% remove an unused member
|
|
|
- Pool = fetch_pool(PoolName, Pools),
|
|
|
+ erlang:demonitor(MRef),
|
|
|
FreePids = lists:delete(Pid, Pool#pool.free_pids),
|
|
|
NumFree = Pool#pool.free_count - 1,
|
|
|
Pool1 = Pool#pool{free_pids = FreePids, free_count = NumFree},
|
|
|
exit(Pid, kill),
|
|
|
- send_metric(<<"pooler.killed_free_count">>, {inc, 1}, counter),
|
|
|
- State#state{pools = store_pool(PoolName, Pool1, Pools),
|
|
|
- all_members = dict:erase(Pid, AllMembers)};
|
|
|
- {ok, {PoolName, CPid, _Time}} ->
|
|
|
- Pool = fetch_pool(PoolName, Pools),
|
|
|
+ send_metric(Pool1, killed_free_count, {inc, 1}, counter),
|
|
|
+ Pool1#pool{all_members = dict:erase(Pid, AllMembers)};
|
|
|
+ {ok, {MRef, CPid, _Time}} ->
|
|
|
+ %% remove a member being consumed. No notice is sent to
|
|
|
+ %% the consumer.
|
|
|
+ erlang:demonitor(MRef),
|
|
|
Pool1 = Pool#pool{in_use_count = Pool#pool.in_use_count - 1},
|
|
|
exit(Pid, kill),
|
|
|
- send_metric(<<"pooler.killed_in_use_count">>, {inc, 1}, counter),
|
|
|
- State#state{pools = store_pool(PoolName, Pool1, Pools),
|
|
|
- consumer_to_pid = cpmap_remove(Pid, CPid, CPMap),
|
|
|
- all_members = dict:erase(Pid, AllMembers)};
|
|
|
+ send_metric(Pool1, killed_in_use_count, {inc, 1}, counter),
|
|
|
+ Pool1#pool{consumer_to_pid = cpmap_remove(Pid, CPid, CPMap),
|
|
|
+ all_members = dict:erase(Pid, AllMembers)};
|
|
|
error ->
|
|
|
- error_logger:error_report({unknown_pid, Pid,
|
|
|
+ error_logger:error_report({{pool, PoolName}, unknown_pid, Pid,
|
|
|
erlang:get_stacktrace()}),
|
|
|
- send_metric(<<"pooler.event">>, unknown_pid, history),
|
|
|
- State
|
|
|
- end.
|
|
|
-
|
|
|
--spec max_free_pool(dict()) -> error_no_members | string().
|
|
|
-max_free_pool(Pools) ->
|
|
|
- case dict:fold(fun fold_max_free_count/3, {"", 0}, Pools) of
|
|
|
- {"", 0} -> error_no_members;
|
|
|
- {MaxFreePoolName, _} -> MaxFreePoolName
|
|
|
+ send_metric(Pool, events, unknown_pid, history),
|
|
|
+ Pool
|
|
|
end.
|
|
|
|
|
|
--spec fold_max_free_count(string(), #pool{}, {string(), non_neg_integer()}) ->
|
|
|
- {string(), non_neg_integer()}.
|
|
|
-fold_max_free_count(Name, Pool, {CName, CMax}) ->
|
|
|
- case Pool#pool.free_count > CMax of
|
|
|
- true -> {Name, Pool#pool.free_count};
|
|
|
- false -> {CName, CMax}
|
|
|
- end.
|
|
|
-
|
|
|
-
|
|
|
--spec start_n_pids(non_neg_integer(), string(), pid(), dict()) ->
|
|
|
- {dict(), [pid()]}.
|
|
|
-start_n_pids(N, PoolName, PoolSup, AllMembers) ->
|
|
|
- NewPids = do_n(N, fun(Acc) ->
|
|
|
- case supervisor:start_child(PoolSup, []) of
|
|
|
- {ok, Pid} ->
|
|
|
- erlang:link(Pid),
|
|
|
- [Pid | Acc];
|
|
|
- _Else ->
|
|
|
- Acc
|
|
|
- end
|
|
|
- end, []),
|
|
|
- AllMembers1 = lists:foldl(
|
|
|
- fun(M, Dict) ->
|
|
|
- Entry = {PoolName, free, os:timestamp()},
|
|
|
- store_all_members(M, Entry, Dict)
|
|
|
- end, AllMembers, NewPids),
|
|
|
- {AllMembers1, NewPids}.
|
|
|
-
|
|
|
-do_n(0, _Fun, Acc) ->
|
|
|
- Acc;
|
|
|
-do_n(N, Fun, Acc) ->
|
|
|
- do_n(N - 1, Fun, Fun(Acc)).
|
|
|
-
|
|
|
-
|
|
|
--spec fetch_pool(string(), dict()) -> #pool{} | error_no_pool.
|
|
|
-fetch_pool(PoolName, Pools) ->
|
|
|
- case dict:find(PoolName, Pools) of
|
|
|
- {ok, Pool} -> Pool;
|
|
|
- error -> error_no_pool
|
|
|
- end.
|
|
|
-
|
|
|
-pool_add_retries(#pool{add_member_retry = Retries}) ->
|
|
|
- Retries;
|
|
|
-pool_add_retries(error_no_pool) ->
|
|
|
- 0.
|
|
|
-
|
|
|
--spec store_pool(string(), #pool{}, dict()) -> dict().
|
|
|
-store_pool(PoolName, Pool = #pool{}, Pools) ->
|
|
|
- dict:store(PoolName, Pool, Pools).
|
|
|
-
|
|
|
-spec store_all_members(pid(),
|
|
|
- {string(), free | pid(), {_, _, _}}, dict()) -> dict().
|
|
|
-store_all_members(Pid, Val = {_PoolName, _CPid, _Time}, AllMembers) ->
|
|
|
+ {reference(), free | pid(), {_, _, _}}, dict()) -> dict().
|
|
|
+store_all_members(Pid, Val = {_MRef, _CPid, _Time}, AllMembers) ->
|
|
|
dict:store(Pid, Val, AllMembers).
|
|
|
|
|
|
-spec set_cpid_for_member(pid(), pid(), dict()) -> dict().
|
|
|
set_cpid_for_member(MemberPid, CPid, AllMembers) ->
|
|
|
dict:update(MemberPid,
|
|
|
- fun({PoolName, free, Time = {_, _, _}}) ->
|
|
|
- {PoolName, CPid, Time}
|
|
|
+ fun({MRef, free, Time = {_, _, _}}) ->
|
|
|
+ {MRef, CPid, Time}
|
|
|
end, AllMembers).
|
|
|
|
|
|
-spec add_member_to_consumer(pid(), pid(), dict()) -> dict().
|
|
|
add_member_to_consumer(MemberPid, CPid, CPMap) ->
|
|
|
- dict:update(CPid, fun(O) -> [MemberPid|O] end, [MemberPid], CPMap).
|
|
|
-
|
|
|
--spec cull_members(string(), #state{}) -> #state{}.
|
|
|
-cull_members(PoolName, #state{pools = Pools} = State) ->
|
|
|
- cull_members_from_pool(fetch_pool(PoolName, Pools), State).
|
|
|
+ %% we can't use dict:update here because we need to create the
|
|
|
+ %% monitor if we aren't already tracking this consumer.
|
|
|
+ case dict:find(CPid, CPMap) of
|
|
|
+ {ok, {MRef, MList}} ->
|
|
|
+ dict:store(CPid, {MRef, [MemberPid | MList]}, CPMap);
|
|
|
+ error ->
|
|
|
+ MRef = erlang:monitor(process, CPid),
|
|
|
+ dict:store(CPid, {MRef, [MemberPid]}, CPMap)
|
|
|
+ end.
|
|
|
|
|
|
--spec cull_members_from_pool(#pool{}, #state{}) -> #state{}.
|
|
|
-cull_members_from_pool(error_no_pool, State) ->
|
|
|
- State;
|
|
|
-cull_members_from_pool(#pool{cull_interval = {0, _}}, State) ->
|
|
|
+-spec cull_members_from_pool(#pool{}) -> #pool{}.
|
|
|
+cull_members_from_pool(#pool{cull_interval = {0, _}} = Pool) ->
|
|
|
%% 0 cull_interval means do not cull
|
|
|
- State;
|
|
|
+ Pool;
|
|
|
cull_members_from_pool(#pool{name = PoolName,
|
|
|
free_count = FreeCount,
|
|
|
init_count = InitCount,
|
|
|
in_use_count = InUseCount,
|
|
|
cull_interval = Delay,
|
|
|
- max_age = MaxAge} = Pool,
|
|
|
- #state{all_members = AllMembers} = State) ->
|
|
|
+ max_age = MaxAge,
|
|
|
+ all_members = AllMembers} = Pool) ->
|
|
|
MaxCull = FreeCount - (InitCount - InUseCount),
|
|
|
- State1 = case MaxCull > 0 of
|
|
|
- true ->
|
|
|
- MemberInfo = member_info(Pool#pool.free_pids, AllMembers),
|
|
|
- ExpiredMembers =
|
|
|
- expired_free_members(MemberInfo, os:timestamp(), MaxAge),
|
|
|
- CullList = lists:sublist(ExpiredMembers, MaxCull),
|
|
|
- lists:foldl(fun({CullMe, _}, S) -> remove_pid(CullMe, S) end,
|
|
|
- State, CullList);
|
|
|
- false ->
|
|
|
- State
|
|
|
- end,
|
|
|
+ Pool1 = case MaxCull > 0 of
|
|
|
+ true ->
|
|
|
+ MemberInfo = member_info(Pool#pool.free_pids, AllMembers),
|
|
|
+ ExpiredMembers =
|
|
|
+ expired_free_members(MemberInfo, os:timestamp(), MaxAge),
|
|
|
+ CullList = lists:sublist(ExpiredMembers, MaxCull),
|
|
|
+ lists:foldl(fun({CullMe, _}, S) -> remove_pid(CullMe, S) end,
|
|
|
+ Pool, CullList);
|
|
|
+ false ->
|
|
|
+ Pool
|
|
|
+ end,
|
|
|
schedule_cull(PoolName, Delay),
|
|
|
- State1.
|
|
|
+ Pool1.
|
|
|
|
|
|
--spec schedule_cull(PoolName :: string(), Delay :: time_spec()) -> reference().
|
|
|
+-spec schedule_cull(PoolName :: atom() | pid(),
|
|
|
+ Delay :: time_spec()) -> reference().
|
|
|
%% @doc Schedule a pool cleaning or "cull" for `PoolName' in which
|
|
|
%% members older than `max_age' will be removed until the pool has
|
|
|
%% `init_count' members. Uses `erlang:send_after/3' for light-weight
|
|
@@ -580,7 +600,7 @@ schedule_cull(PoolName, Delay) ->
|
|
|
DelayMillis = time_as_millis(Delay),
|
|
|
%% use pid instead of server name atom to take advantage of
|
|
|
%% automatic cancelling
|
|
|
- erlang:send_after(DelayMillis, self(), {cull_pool, PoolName}).
|
|
|
+ erlang:send_after(DelayMillis, PoolName, cull_pool).
|
|
|
|
|
|
-spec member_info([pid()], dict()) -> [{pid(), member_info()}].
|
|
|
member_info(Pids, AllMembers) ->
|
|
@@ -594,22 +614,27 @@ expired_free_members(Members, Now, MaxAge) ->
|
|
|
[ MI || MI = {_, {_, free, LastReturn}} <- Members,
|
|
|
timer:now_diff(Now, LastReturn) >= MaxMicros ].
|
|
|
|
|
|
--spec send_metric(Name :: metric_label(),
|
|
|
- Value :: metric_value(),
|
|
|
- Type :: metric_type()) -> ok.
|
|
|
%% Send a metric using the metrics module from application config or
|
|
|
%% do nothing.
|
|
|
-send_metric(Name, Value, Type) ->
|
|
|
- case application:get_env(pooler, metrics_module) of
|
|
|
- undefined -> ok;
|
|
|
- {ok, Mod} -> Mod:notify(Name, Value, Type)
|
|
|
- end,
|
|
|
+-spec send_metric(Pool :: #pool{},
|
|
|
+ Label :: atom(),
|
|
|
+ Value :: metric_value(),
|
|
|
+ Type :: metric_type()) -> ok.
|
|
|
+send_metric(#pool{metrics_mod = pooler_no_metrics}, _Label, _Value, _Type) ->
|
|
|
+ ok;
|
|
|
+send_metric(#pool{name = PoolName, metrics_mod = MetricsMod}, Label, Value, Type) ->
|
|
|
+ MetricName = pool_metric(PoolName, Label),
|
|
|
+ MetricsMod:notify(MetricName, Value, Type),
|
|
|
ok.
|
|
|
|
|
|
--spec pool_metric(string(), 'free_count' | 'in_use_count' | 'take_rate') -> binary().
|
|
|
+-spec pool_metric(atom(), atom()) -> binary().
|
|
|
pool_metric(PoolName, Metric) ->
|
|
|
- iolist_to_binary([<<"pooler.">>, PoolName, ".",
|
|
|
- atom_to_binary(Metric, utf8)]).
|
|
|
+ iolist_to_binary([<<"pooler.">>, atom_to_binary(PoolName, utf8),
|
|
|
+ ".", atom_to_binary(Metric, utf8)]).
|
|
|
+
|
|
|
+-spec time_as_secs(time_spec()) -> non_neg_integer().
|
|
|
+time_as_secs({Time, Unit}) ->
|
|
|
+ time_as_micros({Time, Unit}) div 1000000.
|
|
|
|
|
|
-spec time_as_millis(time_spec()) -> non_neg_integer().
|
|
|
%% @doc Convert time unit into milliseconds.
|
|
@@ -626,3 +651,6 @@ time_as_micros({Time, ms}) ->
|
|
|
1000 * Time;
|
|
|
time_as_micros({Time, mu}) ->
|
|
|
Time.
|
|
|
+
|
|
|
+secs_between({Mega1, Secs1, _}, {Mega2, Secs2, _}) ->
|
|
|
+ (Mega2 - Mega1) * 1000000 + (Secs2 - Secs1).
|