diff --git a/.gitignore b/.gitignore index 02ebb9e..aa7dfd6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .rebar _build ebin +doc diff --git a/Makefile b/Makefile index 1d192a2..2a19bd0 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,11 @@ clean: dialyze: @$(REBAR) dialyzer + +edoc: + mkdir doc && cp -fR doc_src/* doc + $(REBAR) edoc + +edoc_private: + mkdir doc && cp -fR doc_src/* doc + $(REBAR) as edoc_private edoc \ No newline at end of file diff --git a/doc_src/overview.edoc b/doc_src/overview.edoc new file mode 100644 index 0000000..52caf7e --- /dev/null +++ b/doc_src/overview.edoc @@ -0,0 +1,6 @@ +@author Devin Torres (devinus) +@author Andrew Thompson (Vagabond) +@author Kurt Williams (onkel-dirtus) +@title poolboy +@doc A hunky Erlang worker pool factory.
Poolboy is a lightweight, generic pooling library for Erlang with a focus on simplicity, performance, and rock-solid disaster recovery. +@copyright Poolboy is available in the public domain (see UNLICENSE). Poolboy is also optionally available under the ISC license (see LICENSE), meant especially for jurisdictions that do not recognize public domain works. diff --git a/doc_src/style.css b/doc_src/style.css new file mode 100644 index 0000000..0d33337 --- /dev/null +++ b/doc_src/style.css @@ -0,0 +1,65 @@ +/* standard EDoc style sheet */ +body { + font-family: Verdana, Arial, Helvetica, sans-serif; + margin-left: .25in; + margin-right: .2in; + margin-top: 0.2in; + margin-bottom: 0.2in; + color: #000000; + background-color: #ffffff; +} +h1,h2 { + margin-left: -0.2in; +} +div.navbar { + background-color: #add8e6; + padding: 0.2em; +} +h2.indextitle { + padding: 0.4em; + background-color: #add8e6; +} +h3.function,h3.typedecl { + background-color: #add8e6; + padding-left: 1em; +} +div.spec { + margin-left: 2em; + + background-color: #eeeeee; +} +a.module { + text-decoration:none +} +a.module:hover { + background-color: #eeeeee; +} +ul.definitions { + list-style-type: none; +} +ul.index { + list-style-type: none; + background-color: #eeeeee; +} + +/* + * Minor style tweaks + */ +ul { + list-style-type: square; +} +table { + border-collapse: collapse; +} +td { + padding: 3px; + vertical-align: middle; +} + +/* +Tune styles +*/ + +code, p>tt, a>tt { + font-size: 1.2em; +} diff --git a/rebar.config b/rebar.config index 1d494ca..2ece6d5 100644 --- a/rebar.config +++ b/rebar.config @@ -11,5 +11,15 @@ {plugins, [ {rebar3_eqc, ".*", {git, "https://github.com/kellymclaughlin/rebar3-eqc-plugin.git", {tag, "0.1.0"}}} ]} - ] -}]}. + ]}, + + {edoc_private, [ + {edoc_opts, [ + {private, true} + ]} + ]} +]}. + +{edoc_opts, [ + {preprocess, true}, {stylesheet, "style.css"} +]}. \ No newline at end of file diff --git a/src/poolboy.erl b/src/poolboy.erl index db20541..79777fb 100644 --- a/src/poolboy.erl +++ b/src/poolboy.erl @@ -13,12 +13,13 @@ -define(TIMEOUT, 5000). -ifdef(pre17). --type pid_queue() :: queue(). +%-type pid_queue() :: queue(). -else. -type pid_queue() :: queue:queue(). -endif. --ifdef(OTP_RELEASE). %% this implies 21 or higher +%% this implies 21 or higher +-ifdef(OTP_RELEASE). -define(EXCEPTION(Class, Reason, Stacktrace), Class:Reason:Stacktrace). -define(GET_STACK(Stacktrace), Stacktrace). -else. @@ -47,16 +48,51 @@ strategy = lifo :: lifo | fifo }). --spec checkout(Pool :: pool()) -> pid(). +%% @private +-type state() :: #state{ + supervisor :: undefined | pid(), + workers :: undefined | pid_queue(), + waiting :: pid_queue(), + monitors :: ets:tid(), + size :: non_neg_integer(), + overflow :: non_neg_integer(), + max_overflow :: non_neg_integer(), + strategy :: lifo | fifo + }. + +%% @doc Removes pool item from the pool. +%% @returns pid of a worker. +%% @equiv checkout(Pool, true) + +-spec checkout(Pool) -> Result when + Pool :: pool(), + Result :: pid(). checkout(Pool) -> checkout(Pool, true). --spec checkout(Pool :: pool(), Block :: boolean()) -> pid() | full. +%% @doc Removes pool item from the pool. +%% Default wait time for a reply is set to `5000'. +%% @returns pid of a worker or `full' atom. +%% @equiv checkout(Pool, Block, TIMEOUT) + +-spec checkout(Pool, Block) -> Result when + Pool :: pool(), + Block :: boolean(), + Result :: pid() | full. checkout(Pool, Block) -> checkout(Pool, Block, ?TIMEOUT). --spec checkout(Pool :: pool(), Block :: boolean(), Timeout :: timeout()) - -> pid() | full. +%% @doc Removes pool item from the pool. +%% @param Timeout is an integer greater than zero that +%% specifies how many milliseconds to wait for a reply, +%% or the atom `infinity' to wait indefinitely. +%% @returns pid of a worker or `full' atom. + +-spec checkout(Pool, Block, Timeout) -> Result when + Pool :: pool(), + Block :: boolean(), + Timeout :: timeout(), + Result :: pid() | full. checkout(Pool, Block, Timeout) -> CRef = make_ref(), try @@ -67,17 +103,39 @@ checkout(Pool, Block, Timeout) -> erlang:raise(Class, Reason, ?GET_STACK(Stacktrace)) end. --spec checkin(Pool :: pool(), Worker :: pid()) -> ok. +%% @doc Asynchronous try to add worker `pid' to the pool. +%% @equiv gen_server:cast(Pool, {checkin, Worker}) + +-spec checkin(Pool, Worker) -> Result when + Pool :: pool(), + Worker :: pid(), + Result :: ok. checkin(Pool, Worker) when is_pid(Worker) -> gen_server:cast(Pool, {checkin, Worker}). --spec transaction(Pool :: pool(), Fun :: fun((Worker :: pid()) -> any())) - -> any(). +%% @doc Run function `Fun' using a worker from the pool. +%% @equiv transaction(Pool, Fun, TIMEOUT) + +-spec transaction(Pool, Fun) -> Result when + Pool :: pool(), + Fun :: fun((Worker) -> any()), + Worker :: pid(), + Result :: any(). transaction(Pool, Fun) -> transaction(Pool, Fun, ?TIMEOUT). --spec transaction(Pool :: pool(), Fun :: fun((Worker :: pid()) -> any()), - Timeout :: timeout()) -> any(). +%% @doc Run function `Fun' using a worker from the pool. +%% `Timeout' is a time in millisecods while the pool get +%% a worker from it. +%% @param Timeout is an integer greater than zero that +%% specifies how many milliseconds to wait getting +%% a worker from the pool. + +-spec transaction(Pool, Fun, Timeout) -> Result when + Pool :: pool(), + Fun :: fun((Worker :: pid()) -> any()), + Timeout :: timeout(), + Result :: any(). transaction(Pool, Fun, Timeout) -> Worker = poolboy:checkout(Pool, true, Timeout), try @@ -86,23 +144,35 @@ transaction(Pool, Fun, Timeout) -> ok = poolboy:checkin(Pool, Worker) end. --spec child_spec(PoolId :: term(), PoolArgs :: proplists:proplist()) - -> supervisor:child_spec(). +%% @doc Create child specification of a supervisor. +%% @equiv child_spec(PoolId, PoolArgs, []) + +-spec child_spec(PoolId, PoolArgs) -> Result when + PoolId :: term(), + PoolArgs :: proplists:proplist(), + Result :: supervisor:child_spec(). child_spec(PoolId, PoolArgs) -> child_spec(PoolId, PoolArgs, []). --spec child_spec(PoolId :: term(), - PoolArgs :: proplists:proplist(), - WorkerArgs :: proplists:proplist()) - -> supervisor:child_spec(). +%% @doc Create child specification of a supervisor. +%% @equiv child_spec(PoolId, PoolArgs, WorkerArgs, tuple) + +-spec child_spec(PoolId, PoolArgs, WorkerArgs) -> Result when + PoolId :: term(), + PoolArgs :: proplists:proplist(), + WorkerArgs :: proplists:proplist(), + Result :: supervisor:child_spec(). child_spec(PoolId, PoolArgs, WorkerArgs) -> child_spec(PoolId, PoolArgs, WorkerArgs, tuple). --spec child_spec(PoolId :: term(), - PoolArgs :: proplists:proplist(), - WorkerArgs :: proplists:proplist(), - ChildSpecFormat :: 'tuple' | 'map') - -> supervisor:child_spec(). +%% @doc Create child specification of a supervisor. + +-spec child_spec(PoolId, PoolArgs, WorkerArgs, ChildSpecFormat) -> Result when + PoolId :: term(), + PoolArgs :: proplists:proplist(), + WorkerArgs :: proplists:proplist(), + ChildSpecFormat :: 'tuple' | 'map', + Result :: supervisor:child_spec(). child_spec(PoolId, PoolArgs, WorkerArgs, tuple) -> {PoolId, {poolboy, start_link, [PoolArgs, WorkerArgs]}, permanent, 5000, worker, [poolboy]}; @@ -114,43 +184,130 @@ child_spec(PoolId, PoolArgs, WorkerArgs, map) -> type => worker, modules => [poolboy]}. --spec start(PoolArgs :: proplists:proplist()) - -> start_ret(). +%% @doc Creates a standalone `gen_server' process, +%% that is, a `gen_server' process that is not part of +%% a supervision tree and thus has no supervisor. +%% @equiv start(PoolArgs, PoolArgs) + +-spec start(PoolArgs) -> Result when + PoolArgs :: proplists:proplist(), + Result :: start_ret(). start(PoolArgs) -> start(PoolArgs, PoolArgs). --spec start(PoolArgs :: proplists:proplist(), - WorkerArgs:: proplists:proplist()) - -> start_ret(). +%% @doc Creates a standalone `gen_server' process, +%% that is, a `gen_server' process that is not part of +%% a supervision tree and thus has no supervisor. + +-spec start(PoolArgs, WorkerArgs) -> Result when + PoolArgs :: proplists:proplist(), + WorkerArgs:: proplists:proplist(), + Result :: start_ret(). start(PoolArgs, WorkerArgs) -> start_pool(start, PoolArgs, WorkerArgs). --spec start_link(PoolArgs :: proplists:proplist()) - -> start_ret(). +%% @doc Creates a `gen_server' process as part of +%% a supervision tree. This function is to be called, +%% directly or indirectly, by the supervisor. +%% For example, it ensures that the gen_server process +%% is linked to the supervisor. +%% @equiv start_link(PoolArgs, PoolArgs) + +-spec start_link(PoolArgs) -> Result when + PoolArgs :: proplists:proplist(), + Result :: start_ret(). start_link(PoolArgs) -> %% for backwards compatability, pass the pool args as the worker args as well start_link(PoolArgs, PoolArgs). --spec start_link(PoolArgs :: proplists:proplist(), - WorkerArgs:: proplists:proplist()) - -> start_ret(). +%% @doc Creates a `gen_server' process as part of +%% a supervision tree. This function is to be called, +%% directly or indirectly, by the supervisor. +%% For example, it ensures that the gen_server process +%% is linked to the supervisor. + +-spec start_link(PoolArgs, WorkerArgs) -> Result when + PoolArgs :: proplists:proplist(), + WorkerArgs:: proplists:proplist(), + Result :: start_ret(). start_link(PoolArgs, WorkerArgs) -> start_pool(start_link, PoolArgs, WorkerArgs). --spec stop(Pool :: pool()) -> ok. +%% @doc Stop pool of workers. +%% @equiv gen_server:call(Pool, stop) + +-spec stop(Pool) -> Result when + Pool :: pool(), + Result :: ok. stop(Pool) -> gen_server:call(Pool, stop). --spec status(Pool :: pool()) -> {atom(), integer(), integer(), integer()}. +%% @doc Get pool description. +%% @equiv gen_server:call(Pool, status) + +-spec status(Pool) -> Result when + Pool :: pool(), + Result :: {StateName, QueueLength, Overflow, Size}, + StateName :: full | overflow | ready, + QueueLength :: non_neg_integer(), + Overflow :: non_neg_integer(), + Size :: non_neg_integer(). + status(Pool) -> gen_server:call(Pool, status). +%% @private +%% @equiv init(PoolArgs, WorkerArgs, state()) + +-spec init(Args) -> Result when + Args :: {PoolArgs, WorkerArgs}, + PoolArgs :: proplists:proplist(), + WorkerArgs:: proplists:proplist(), + Result :: {ok, state()}. init({PoolArgs, WorkerArgs}) -> process_flag(trap_exit, true), Waiting = queue:new(), Monitors = ets:new(monitors, [private]), init(PoolArgs, WorkerArgs, #state{waiting = Waiting, monitors = Monitors}). +-spec init(List, WorkerArgs, State) -> Result when + List :: [{worker_module, Mod} | Rest], + Mod :: module(), + Rest :: list(), + WorkerArgs:: proplists:proplist(), + State :: state(), + Result :: {ok, state()}; +(List, WorkerArgs, State) -> Result when + List :: [{size, Size} | Rest], + Size :: non_neg_integer(), + Rest :: list(), + WorkerArgs:: proplists:proplist(), + State :: state(), + Result :: {ok, state()}; +(List, WorkerArgs, State) -> Result when + List :: [{max_overflow, MaxOverflow} | Rest], + MaxOverflow :: non_neg_integer(), + Rest :: list(), + WorkerArgs:: proplists:proplist(), + State :: state(), + Result :: {ok, state()}; +(List, WorkerArgs, State) -> Result when + List :: [{strategy, lifo} | Rest], + Rest :: list(), + WorkerArgs:: proplists:proplist(), + State :: state(), + Result :: {ok, state()}; +(List, WorkerArgs, State) -> Result when + List :: [{strategy, fifo} | Rest], + Rest :: list(), + WorkerArgs:: proplists:proplist(), + State :: state(), + Result :: {ok, state()}; +(List, WorkerArgs, State) -> Result when + List :: list(), + WorkerArgs:: proplists:proplist(), + State :: state(), + Result :: {ok, state()}. init([{worker_module, Mod} | Rest], WorkerArgs, State) when is_atom(Mod) -> {ok, Sup} = poolboy_sup:start_link(Mod, WorkerArgs), init(Rest, WorkerArgs, State#state{supervisor = Sup}); @@ -168,6 +325,22 @@ init([], _WorkerArgs, #state{size = Size, supervisor = Sup} = State) -> Workers = prepopulate(Size, Sup), {ok, State#state{workers = Workers}}. +%% @private + +-spec handle_cast(Request, State) -> Result when + Request :: {checkin, Pid}, + Pid :: pid(), + State :: state(), + Result :: {noreply, State}; +(Request, State) -> Result when + Request :: {cancel_waiting, CRef}, + CRef :: reference(), + State :: state(), + Result :: {noreply, State}; +(Request, State) -> Result when + Request :: term(), + State :: state(), + Result :: {noreply, State}. handle_cast({checkin, Pid}, State = #state{monitors = Monitors}) -> case ets:lookup(Monitors, Pid) of [{Pid, _, MRef}] -> @@ -200,6 +373,52 @@ handle_cast({cancel_waiting, CRef}, State) -> handle_cast(_Msg, State) -> {noreply, State}. +%% @private + +-spec handle_call(Request, From, State) -> Result when + Request :: {checkout, CRef, Block}, + CRef :: reference(), + Block :: boolean(), + From :: {pid(), atom()}, + State :: state(), + Result :: {reply, Pid, state()} | {reply, full, state()} | {noreply, state()}, + Pid :: pid(); +(Request, From, State) -> Result when + Request :: status, + From :: {pid(), atom()}, + State :: state(), + Result :: {reply, {StateName, QueueLength, Overflow, Value}, State}, + StateName :: full | overflow | ready, + QueueLength :: non_neg_integer(), + Overflow :: non_neg_integer(), + Value :: term(); +(Request, From, State) -> Result when + Request :: get_avail_workers, + From :: {pid(), atom()}, + State :: state(), + Result :: {reply, Workers, State}, + Workers :: undefined | pid_queue(); +(Request, From, State) -> Result when + Request :: get_all_workers, + From :: {pid(), atom()}, + State :: state(), + Result :: {reply, WorkerList, State}, + WorkerList :: [{Id, Child, Type, Modules}], + Id :: supervisor:child_id() | undefined, + Child :: supervisor:child() | restarting, + Type :: supervisor:worker(), + Modules :: supervisor:modules(); +(Request, From, State) -> Result when + Request :: get_all_monitors, + From :: {pid(), atom()}, + State :: state(), + Result :: {reply, Monitors, State}, + Monitors :: ets:tid(); +(Request, From, State) -> Result when + Request :: stop, + From :: {pid(), atom()}, + State :: state(), + Result :: {stop, normal, ok, State}. handle_call({checkout, CRef, Block}, {FromPid, _} = From, State) -> #state{supervisor = Sup, workers = Workers, @@ -247,6 +466,23 @@ handle_call(_Msg, _From, State) -> Reply = {error, invalid_message}, {reply, Reply, State}. +%% @private + +-spec handle_info(Info, State) -> Result when + Info :: {'DOWN', MonitorRef, any(), any(), any()}, + MonitorRef :: reference(), + State :: state(), + Result :: {noreply, state()}; +(Info, State) -> Result when + Info :: {'EXIT', Pid, Reason}, + Pid :: pid(), + Reason :: term(), + State :: state(), + Result :: {noreply, state()}; +(Info, State) -> Result when + Info :: timeout | term(), + State :: state(), + Result :: {noreply, state()}. handle_info({'DOWN', MRef, _, _, _}, State) -> case ets:match(State#state.monitors, {'$1', '_', MRef}) of [[Pid]] -> @@ -279,15 +515,35 @@ handle_info({'EXIT', Pid, _Reason}, State) -> handle_info(_Info, State) -> {noreply, State}. +%% @private + +-spec terminate(Reason, State) -> Result when + Reason :: normal | shutdown | {shutdown,term()} | term(), + State :: state(), + Result :: ok. terminate(_Reason, State) -> Workers = queue:to_list(State#state.workers), ok = lists:foreach(fun (W) -> unlink(W) end, Workers), true = exit(State#state.supervisor, shutdown), ok. +%% @private +- spec code_change(OldVsn, State, Extra) -> Result when + OldVsn :: Vsn | {down, Vsn}, + Vsn :: term(), + State :: state(), + Extra :: term(), + Result :: {ok, NewState} | {error, Reason}, + NewState :: term(), + Reason :: term(). code_change(_OldVsn, State, _Extra) -> {ok, State}. +-spec start_pool(StartFun, PoolArgs, WorkerArgs) -> Result when + StartFun :: start | start_link, + PoolArgs :: proplists:proplist(), + WorkerArgs:: proplists:proplist(), + Result :: start_ret(). start_pool(StartFun, PoolArgs, WorkerArgs) -> case proplists:get_value(name, PoolArgs) of undefined -> @@ -296,38 +552,73 @@ start_pool(StartFun, PoolArgs, WorkerArgs) -> gen_server:StartFun(Name, ?MODULE, {PoolArgs, WorkerArgs}, []) end. +-spec new_worker(Supervisor) -> Result when + Supervisor :: pid(), + Result :: pid(). new_worker(Sup) -> {ok, Pid} = supervisor:start_child(Sup, []), true = link(Pid), Pid. +-spec new_worker(Supervisor, FromPid) -> Result when + Supervisor :: pid(), + FromPid :: pid(), + Result :: pid(). new_worker(Sup, FromPid) -> Pid = new_worker(Sup), Ref = erlang:monitor(process, FromPid), {Pid, Ref}. +-spec get_worker_with_strategy(Workers, Strategy) -> Result when + Workers :: pid_queue(), + Strategy :: lifo | fifo, + Result :: {Value, Left} | {Value, Left}, + Value :: empty | {value, pid()}, + Left :: pid_queue(). get_worker_with_strategy(Workers, fifo) -> queue:out(Workers); get_worker_with_strategy(Workers, lifo) -> queue:out_r(Workers). +-spec dismiss_worker(Supervisor, Pid) -> Result when + Supervisor :: pid(), + Pid :: pid(), + Result :: ok | {error, Error}, + Error :: not_found | simple_one_for_one. dismiss_worker(Sup, Pid) -> true = unlink(Pid), supervisor:terminate_child(Sup, Pid). +-spec filter_worker_by_pid(Pid, Workers) -> Result when + Pid :: pid(), + Workers :: pid_queue(), + Result :: pid_queue(). filter_worker_by_pid(Pid, Workers) -> queue:filter(fun (WPid) -> WPid =/= Pid end, Workers). +-spec prepopulate(Count, Supervisor) -> Result when + Count :: integer(), + Supervisor :: pid(), + Result :: pid_queue(). prepopulate(N, _Sup) when N < 1 -> queue:new(); prepopulate(N, Sup) -> prepopulate(N, Sup, queue:new()). +-spec prepopulate(Count, Supervisor, Workers) -> Result when + Count :: integer(), + Supervisor :: pid(), + Workers :: pid_queue(), + Result :: pid_queue(). prepopulate(0, _Sup, Workers) -> Workers; prepopulate(N, Sup, Workers) -> prepopulate(N-1, Sup, queue:in(new_worker(Sup), Workers)). +-spec handle_checkin(Pid, State) -> Result when + Pid :: pid(), + State :: state(), + Result :: state(). handle_checkin(Pid, State) -> #state{supervisor = Sup, waiting = Waiting, @@ -346,6 +637,10 @@ handle_checkin(Pid, State) -> State#state{workers = Workers, waiting = Empty, overflow = 0} end. +-spec handle_worker_exit(Pid, State) -> Result when + Pid :: pid(), + State :: state(), + Result :: state(). handle_worker_exit(Pid, State) -> #state{supervisor = Sup, monitors = Monitors, @@ -364,6 +659,9 @@ handle_worker_exit(Pid, State) -> State#state{workers = Workers, waiting = Empty} end. +-spec state_name(State) -> Result when + State :: state(), + Result :: full | overflow | ready. state_name(State = #state{overflow = Overflow}) when Overflow < 1 -> #state{max_overflow = MaxOverflow, workers = Workers} = State, case queue:len(Workers) == 0 of diff --git a/src/poolboy_sup.erl b/src/poolboy_sup.erl index e6485a6..d9612aa 100644 --- a/src/poolboy_sup.erl +++ b/src/poolboy_sup.erl @@ -5,9 +5,23 @@ -export([start_link/2, init/1]). +-spec start_link(Mod, Args) -> Result when + Mod :: module(), + Args :: term(), + Result :: {ok, pid()} | ignore | {error, StartlinkError}, + StartlinkError :: {already_started, pid()} | {shutdown, term()} | term(). start_link(Mod, Args) -> supervisor:start_link(?MODULE, {Mod, Args}). +%% @private + +-spec init(Tuple) -> Result when + Tuple :: {Mod, Args}, + Mod :: module(), + Args :: term(), + Result :: {ok,{SupFlags, ChildSpec}} | ignore, + SupFlags :: {simple_one_for_one, 0, 1}, + ChildSpec :: [supervisor:child_spec()]. init({Mod, Args}) -> {ok, {{simple_one_for_one, 0, 1}, [{Mod, {Mod, start_link, [Args]},