From 79ed8004fb47d171071045eaa27a7dc4a6a10aa1 Mon Sep 17 00:00:00 2001 From: Anatolii Kosorukov Date: Mon, 25 Apr 2022 15:48:56 +0300 Subject: [PATCH 1/2] Documentation improvements Add -spec tag to all function in poolboy module. Improving the readability of the description of the specification (-spec) of function parameters. Add the use of @equiv tag to increase understanding of how the function works. Add edoc, edoc_private tasks to Makefile. --- .gitignore | 1 + Makefile | 8 + doc_src/overview.edoc | 6 + doc_src/style.css | 65 ++++++++ rebar.config | 14 +- src/poolboy.erl | 366 ++++++++++++++++++++++++++++++++++++++---- src/poolboy_sup.erl | 14 ++ 7 files changed, 438 insertions(+), 36 deletions(-) create mode 100644 doc_src/overview.edoc create mode 100644 doc_src/style.css 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]}, From f2c29dcc14bef77c094c18f89a28f5ae27dc479d Mon Sep 17 00:00:00 2001 From: Anatolii Kosorukov Date: Wed, 27 Apr 2022 19:47:14 +0300 Subject: [PATCH 2/2] A property tests Rethink EUnit tests (reproduce EUnit tests as proper tests, fixing expected state of worker pool). Fix error: ===> Errors loading plugin {rebar3_eqc,".*", {git, "https://github.com/kellymclaughlin/rebar3-eqc-plugin.git", {tag,"0.1.0"}}}. Run rebar3 with DEBUG=1 set to see errors. Note: poolboy_eqc.erl is not worked. I would be nice rewrite it or remove it. Add documentation to status(Pool) function. --- Makefile | 1 + test/poolboy_eqc.erl => poolboy_eqc.erl | 0 rebar.config | 10 +- src/poolboy.erl | 6 + test/prop_poolboy.erl | 960 ++++++++++++++++++++++++ 5 files changed, 975 insertions(+), 2 deletions(-) rename test/poolboy_eqc.erl => poolboy_eqc.erl (100%) create mode 100644 test/prop_poolboy.erl diff --git a/Makefile b/Makefile index 2a19bd0..5165567 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ compile: test: @$(REBAR) eunit + @$(REBAR) proper -n 1 qc: compile @$(REBAR) eqc diff --git a/test/poolboy_eqc.erl b/poolboy_eqc.erl similarity index 100% rename from test/poolboy_eqc.erl rename to poolboy_eqc.erl diff --git a/rebar.config b/rebar.config index 2ece6d5..d5b4963 100644 --- a/rebar.config +++ b/rebar.config @@ -10,7 +10,11 @@ {test, [ {plugins, [ {rebar3_eqc, ".*", {git, "https://github.com/kellymclaughlin/rebar3-eqc-plugin.git", {tag, "0.1.0"}}} - ]} + ]}, + {deps, [ + {proper,"1.4.0"}, + {rebar3_eqc,"1.3.0"} + ]} ]}, {edoc_private, [ @@ -22,4 +26,6 @@ {edoc_opts, [ {preprocess, true}, {stylesheet, "style.css"} -]}. \ No newline at end of file +]}. + +{project_plugins, [rebar3_proper,rebar3_eqc]}. diff --git a/src/poolboy.erl b/src/poolboy.erl index 79777fb..e74e5b6 100644 --- a/src/poolboy.erl +++ b/src/poolboy.erl @@ -243,6 +243,12 @@ stop(Pool) -> gen_server:call(Pool, stop). %% @doc Get pool description. +%% @returns Result :: {StateName, QueueLength, Overflow, Size} +%%
+%% StateName: status of the pool
+%% QueueLength: length of worker queue
+%% Overflow: maximum number of workers created if pool is empty
+%% Size: The number of objects inserted in the table
%% @equiv gen_server:call(Pool, status) -spec status(Pool) -> Result when diff --git a/test/prop_poolboy.erl b/test/prop_poolboy.erl new file mode 100644 index 0000000..cc5fd01 --- /dev/null +++ b/test/prop_poolboy.erl @@ -0,0 +1,960 @@ +-module(prop_poolboy). +-include_lib("proper/include/proper.hrl"). + +%%%%%%%%%%%%%%%%%% +%%% Properties %%% +%%%%%%%%%%%%%%%%%% + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_startup +%% @doc Basic pool operations +prop_pool_startup() -> + ?FORALL({Size,MaxOverflow}, ?SUCHTHAT({Size, MaxOverflow}, {range(2,100), range(1,100)}, Size / 2 >= MaxOverflow), + begin + {ok, Pool} = new_pool(Size, MaxOverflow), + + %% Check basic pool operation. + WorkerCount = queue:len(pool_call(Pool, get_avail_workers)), + WorkerCount = Size, + poolboy:checkout(Pool), + WorkerCount_1 = queue:len(pool_call(Pool, get_avail_workers)), + WorkerCount_1 = WorkerCount - 1, + Worker = poolboy:checkout(Pool), + WorkerCount_2 = queue:len(pool_call(Pool, get_avail_workers)), + WorkerCount_2 = WorkerCount - 2, + checkin_worker(Pool, Worker), + WorkerCount_1 = queue:len(pool_call(Pool, get_avail_workers)), + + MonitorCount = length(pool_call(Pool, get_all_monitors)), + MonitorCount = 1, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_overflow +%% @doc Pool overflow should work +prop_pool_overflow() -> + ?FORALL({Size,MaxOverflow}, ?SUCHTHAT({Size, MaxOverflow}, {range(8,100), range(8,100)}, Size / 2 >= MaxOverflow), + %?FORALL(_Bool, boolean(), + begin + %% Check that the pool overflows properly. + {ok, Pool} = new_pool(Size, MaxOverflow), + % make a list of workers + Workers = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size+1)], + Avail_workers = pool_call(Pool, get_avail_workers), + All_workers = pool_call(Pool, get_all_workers), + Avail_workersLength = queue:len(Avail_workers), + All_workersLength = length(All_workers), + + % io:format("Avail_workers=~p~n",[pool_call(Pool, get_avail_workers)]), + % io:format("All_workers=~p~n",[pool_call(Pool, get_all_workers)]), + % io:format("All_monitors=~p~n",[pool_call(Pool, get_all_monitors)]), + % io:format("Avail_workers length=~p~n",[queue:len(pool_call(Pool, get_avail_workers))]), + % io:format("All_workers length=~p~n",[length(pool_call(Pool, get_all_workers))]), + % io:format("All_monitors length=~p~n~n",[length(pool_call(Pool, get_all_monitors))]), + + Avail_workersLength = 0, + All_workersLength = Size+2, + + [A, B, C, D, E, F, G|_Rest] = Workers, + + checkin_worker(Pool, A), + checkin_worker(Pool, B), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) + 0, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + % io:format("=> Check in A, Check in B~n",[]), + % io:format("Avail_workers=~p~n",[pool_call(Pool, get_avail_workers)]), + % io:format("All_workers=~p~n",[pool_call(Pool, get_all_workers)]), + % io:format("Avail_workers length=~p~n",[queue:len(pool_call(Pool, get_avail_workers))]), + % io:format("All_workers length=~p~n",[length(pool_call(Pool, get_all_workers))]), + % io:format("All_monitors length=~p~n~n",[length(pool_call(Pool, get_all_monitors))]), + + checkin_worker(Pool, C), + checkin_worker(Pool, D), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 2, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + % io:format("=> Check in C, Check in D~n",[]), + % io:format("Avail_workers=~p~n",[pool_call(Pool, get_avail_workers)]), + % io:format("All_workers=~p~n",[pool_call(Pool, get_all_workers)]), + % io:format("Avail_workers length=~p~n",[queue:len(pool_call(Pool, get_avail_workers))]), + % io:format("All_workers length=~p~n",[length(pool_call(Pool, get_all_workers))]), + % io:format("All_monitors length=~p~n~n",[length(pool_call(Pool, get_all_monitors))]), + + checkin_worker(Pool, E), + checkin_worker(Pool, F), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 4, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + % io:format("=> Check in E, Check in F~n",[]), + % io:format("Avail_workers=~p~n",[pool_call(Pool, get_avail_workers)]), + % io:format("All_workers=~p~n",[pool_call(Pool, get_all_workers)]), + % io:format("Avail_workers length=~p~n",[queue:len(pool_call(Pool, get_avail_workers))]), + % io:format("All_workers length=~p~n",[length(pool_call(Pool, get_all_workers))]), + % io:format("All_monitors length=~p~n~n",[length(pool_call(Pool, get_all_monitors))]), + + checkin_worker(Pool, G), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 5, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + % io:format("=> Check in G~n",[]), + % io:format("Avail_workers=~p~n",[pool_call(Pool, get_avail_workers)]), + % io:format("All_workers=~p~n",[pool_call(Pool, get_all_workers)]), + % io:format("Avail_workers length=~p~n",[queue:len(pool_call(Pool, get_avail_workers))]), + % io:format("All_workers length=~p~n",[length(pool_call(Pool, get_all_workers))]), + % io:format("All_monitors length=~p~n~n",[length(pool_call(Pool, get_all_monitors))]), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_empty_no_overflow +%% @doc Pool behaves when empty and oveflow is disabled +prop_pool_empty_no_overflow() -> + ?FORALL(Size, range(5,100), + begin + %% Checks the pool handles the empty condition properly when overflow is + %% disabled. + {ok, Pool} = new_pool(Size, 0), + Workers = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size-1)], + Avail_workers = pool_call(Pool, get_avail_workers), + All_workers = pool_call(Pool, get_all_workers), + Avail_workersLength = queue:len(Avail_workers), + All_workersLength = length(All_workers), + + Avail_workersLength = 0, + All_workersLength = Size, + + [A, B, C, D, E | _Rest] = Workers, + Self = self(), + spawn(fun() -> + Worker = poolboy:checkout(Pool), + Self ! got_worker, + checkin_worker(Pool, Worker) + end), + + %% Spawned process should block waiting for worker to be available. + BlockWaitingResult = true, + receive + got_worker -> + case BlockWaitingResult == false of + true -> + throw(badarg); + false -> ok + end + after + 500 -> BlockWaitingResult = true + end, + + checkin_worker(Pool, A), + checkin_worker(Pool, B), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 2, + All_workersLength = length(pool_call(Pool, get_all_workers)), + + %% Spawned process should have been able to obtain a worker. + receive + got_worker -> + case BlockWaitingResult == true of + false -> + throw(badarg); + true -> ok + end + after + 500 -> throw(badarg) + end, + + checkin_worker(Pool, C), + checkin_worker(Pool, D), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 4, + All_workersLength = length(pool_call(Pool, get_all_workers)), + + checkin_worker(Pool, E), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 5, + All_workersLength = length(pool_call(Pool, get_all_workers)), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_empty +%% @doc Pool behaves when empty +prop_pool_empty() -> + ?FORALL({Size,MaxOverflow}, ?SUCHTHAT({Size, MaxOverflow}, {range(8,100), range(8,100)}, Size / 2 >= MaxOverflow), + %?FORALL(_Bool, boolean(), + begin + %% Check that the pool overflows properly. + {ok, Pool} = new_pool(Size, MaxOverflow), + + Workers = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size+1)], + Avail_workers = pool_call(Pool, get_avail_workers), + All_workers = pool_call(Pool, get_all_workers), + Avail_workersLength = queue:len(Avail_workers), + All_workersLength = length(All_workers), + + Avail_workersLength = 0, + All_workersLength = Size+2, + + + [A, B, C, D, E, F, G|_Rest] = Workers, + + Self = self(), + spawn(fun() -> + Worker = poolboy:checkout(Pool), + Self ! got_worker, + checkin_worker(Pool, Worker) + end), + + %% Spawned process should block waiting for worker to be available. + BlockWaitingResult = true, + receive + got_worker -> + case BlockWaitingResult == false of + true -> + throw(badarg); + false -> ok + end + after + 500 -> BlockWaitingResult = true + end, + + checkin_worker(Pool, A), + checkin_worker(Pool, B), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) + 0, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + spawn(fun() -> + Worker = poolboy:checkout(Pool), + Self ! got_worker, + checkin_worker(Pool, Worker) + end), + + %% Spawned process should have been able to obtain a worker. + receive + got_worker -> + case BlockWaitingResult == true of + false -> + throw(badarg); + true -> ok + end + after + 500 -> throw(badarg) + end, + + checkin_worker(Pool, C), + checkin_worker(Pool, D), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 2, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + checkin_worker(Pool, E), + checkin_worker(Pool, F), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 4, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + checkin_worker(Pool, G), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - 5, + All_workersLength = length(pool_call(Pool, get_all_workers)) + 2, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_worker_death +%% @doc Pool behaves on worker death +prop_pool_worker_death() -> + ?FORALL({Size,MaxOverflow}, ?SUCHTHAT({Size, MaxOverflow}, {range(2,100), range(1,100)}, Size / 2 >= MaxOverflow), + %?FORALL(_Bool, boolean(), + begin + %% Check that dead workers are only restarted when the pool is not full + %% and the overflow count is 0. Meaning, don't restart overflow workers. + {ok, Pool} = new_pool(Size, MaxOverflow), + + Worker = poolboy:checkout(Pool), + kill_worker(Worker), + + All_workers = pool_call(Pool, get_all_workers), + All_workersLength = length(All_workers), + + All_workersLength = Size, + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - Size, + [A, B, C|_Workers] = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size+1)], + Avail_workers = pool_call(Pool, get_avail_workers), + Avail_workersLength = queue:len(Avail_workers), + Avail_workersLength = 0, + All_workersLength = length(pool_call(Pool, get_all_workers))-2, + + kill_worker(A), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + All_workersLength = length(pool_call(Pool, get_all_workers))-1, + kill_worker(B), + kill_worker(C), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers))-1, + All_workersLength = length(pool_call(Pool, get_all_workers)), + MonitorCount = length(pool_call(Pool, get_all_monitors)), + MonitorCount = Size - 1, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_worker_death_while_full +%% @doc Pool behaves when full and a worker dies +prop_worker_death_while_full() -> + ?FORALL({Size,MaxOverflow}, ?SUCHTHAT({Size, MaxOverflow}, {range(2,100), range(1,100)}, Size / 2 >= MaxOverflow), + begin + %% Check that if a worker dies while the pool is full and there is a + %% queued checkout, a new worker is started and the checkout serviced. + %% If there are no queued checkouts, a new worker is not started. + {ok, Pool} = new_pool(Size, MaxOverflow), + Worker = poolboy:checkout(Pool), + kill_worker(Worker), + + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - Size, + All_workers = pool_call(Pool, get_all_workers), + All_workersLength = length(All_workers), + All_workersLength = Size, + + [A, B|_Workers] = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size+1)], + Avail_workers = pool_call(Pool, get_avail_workers), + Avail_workersLength = queue:len(Avail_workers), + Avail_workersLength = 0, + All_workersLength = length(pool_call(Pool, get_all_workers))-2, + + Self = self(), + spawn(fun() -> + poolboy:checkout(Pool), + Self ! got_worker, + %% XXX: Don't release the worker. We want to also test what happens + %% when the worker pool is full and a worker dies with no queued + %% checkouts. + timer:sleep(5000) + end), + + %% Spawned process should block waiting for worker to be available. + BlockWaitingResult = true, + + kill_worker(A), + + %% Spawned process should have been able to obtain a worker. + receive + got_worker -> + case BlockWaitingResult == true of + false -> + throw(badarg); + true -> ok + end + after + 1000 -> throw(badarg) + end, + + kill_worker(B), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + %io:format("All_workersLength = ~p, Size=~p~n",[length(pool_call(Pool, get_all_workers)),Size]), + All_workersLength = length(pool_call(Pool, get_all_workers))-1, + MonitorCount = length(pool_call(Pool, get_all_monitors)), + MonitorCount = Size + 1, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_worker_death_while_full_no_overflow +%% @doc Pool behaves when full, a worker dies and overflow disabled +prop_worker_death_while_full_no_overflow() -> + ?FORALL(Size, range(5,100), + begin + %% Check that if a worker dies while the pool is full and there's no + %% overflow, a new worker is started unconditionally and any queued + %% checkouts are serviced. + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + Worker = poolboy:checkout(Pool), + kill_worker(Worker), + + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) - Size, + All_workers = pool_call(Pool, get_all_workers), + All_workersLength = length(All_workers), + All_workersLength = Size, + + [A, B, C|_Workers] = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size - 1)], + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + All_workersLength = length(pool_call(Pool, get_all_workers)), + + Self = self(), + spawn(fun() -> + poolboy:checkout(Pool), + Self ! got_worker, + %% XXX: Do not release, need to also test when worker dies and no + %% checkouts queued. + timer:sleep(5000) + end), + + %% Spawned process should block waiting for worker to be available. + BlockWaitingResult = true, + receive + got_worker -> + case BlockWaitingResult == false of + true -> + throw(badarg); + false -> ok + end + after + 500 -> BlockWaitingResult = true + end, + + kill_worker(A), + + %% Spawned process should have been able to obtain a worker. + receive + got_worker -> + case BlockWaitingResult == true of + false -> + throw(badarg); + true -> ok + end + after + 1000 -> throw(badarg) + end, + + kill_worker(B), + + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers))-1, + All_workersLength = length(pool_call(Pool, get_all_workers)), + + kill_worker(C), + + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers))-2, + All_workersLength = length(pool_call(Pool, get_all_workers)), + MonitorCount = length(pool_call(Pool, get_all_monitors)), + MonitorCount = Size - 2, + + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_full_nonblocking_no_overflow +%% @doc Non-blocking pool behaves when full and overflow disabled +prop_pool_full_nonblocking_no_overflow()-> + ?FORALL(Size, range(5,100), + begin + %% Check that when the pool is full, checkouts return 'full' when the + %% option to use non-blocking checkouts is used. + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + Workers = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size - 1)], + + Avail_workersLength = 0, + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + Size = length(pool_call(Pool, get_all_workers)), + full = poolboy:checkout(Pool, false), + full = poolboy:checkout(Pool, false), + + A = hd(Workers), + checkin_worker(Pool, A), + MonitorCount = length(pool_call(Pool, get_all_monitors)), + %io:format("MonitorCount=~p, Size=~p~n",[MonitorCount, Size]), + MonitorCount = Size-1, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_full_nonblocking +%% @doc Non-blocking pool behaves when full +prop_pool_full_nonblocking()-> + ?FORALL(Size, range(5,5), + begin + %% Check that when the pool is full, checkouts return 'full' when the + %% option to use non-blocking checkouts is used. + MaxOverflow = Size, + {ok, Pool} = new_pool(Size, MaxOverflow), + + Workers = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size+4)], + Avail_workersLength = 0, + All_workersLength = Size, + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + All_workersLength = length(pool_call(Pool, get_all_workers))-5, + %io:format("All_workersLength =~p, Size=~p~n",[length(pool_call(Pool, get_all_workers)),Size]), + full = poolboy:checkout(Pool, false), + %io:format("State =~p~n",[poolboy:checkout(Pool, false)]), + A = hd(Workers), + checkin_worker(Pool, A), + NewWorker = poolboy:checkout(Pool, false), + + %% Overflow workers get shutdown + false = is_process_alive(A), + true = is_pid(NewWorker), + full = poolboy:checkout(Pool, false), + %io:format("State =~p~n",[poolboy:checkout(Pool, false)]), + MonitorCount = length(pool_call(Pool, get_all_monitors)), + MonitorCount = Size+5, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_owner_death +%% @doc Pool behaves on owner death +prop_owner_death()-> + ?FORALL(Size, range(5,100), + begin + %% Check that a dead owner (a process that dies with a worker checked out) + %% causes the pool to dismiss the worker and prune the state space. + MaxOverflow = Size, + {ok, Pool} = new_pool(Size, MaxOverflow), + spawn(fun() -> + poolboy:checkout(Pool), + receive after 500 -> exit(normal) end + end), + timer:sleep(1000), + + Size = queue:len(pool_call(Pool, get_avail_workers)), + Size = length(pool_call(Pool, get_all_workers)), + MonitorCount = length(pool_call(Pool, get_all_monitors)), + MonitorCount = 0, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_checkin_after_exception_in_transaction +%% @doc Worker checked-in after an exception in a transaction +prop_checkin_after_exception_in_transaction() -> + ?FORALL(Size, range(5,100), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + Tx = fun(Worker) -> + true = is_pid(Worker), + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)) + 1, + throw(it_on_the_ground) + end, + + try + poolboy:transaction(Pool, Tx) + catch + throw:it_on_the_ground -> ok + end, + Avail_workersLength = queue:len(pool_call(Pool, get_avail_workers)), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_pool_returns_status +%% @doc Pool returns status +prop_pool_returns_status() -> + ?FORALL(Size, range(5,100), + begin + fun() -> + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + fun() -> + StateName = ready, + QueueLength = Size, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + poolboy:checkout(Pool), + + fun() -> + StateName = ready, + QueueLength = Size - 1, + Overflow = 0, + CurrentSize = 1, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + poolboy:checkout(Pool), + + fun() -> + StateName = ready, + QueueLength = Size - 2, + Overflow = 0, + CurrentSize = 2, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + ok = poolboy:stop(Pool) + end(), + + fun() -> + MaxOverflow = Size, + {ok, Pool} = new_pool(Size, MaxOverflow), + fun() -> + StateName = ready, + QueueLength = Size, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + poolboy:checkout(Pool), + + fun() -> + StateName = ready, + QueueLength = Size - 1, + Overflow = 0, + CurrentSize = 1, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + poolboy:checkout(Pool), + + fun() -> + StateName = ready, + QueueLength = Size - 2, + Overflow = 0, + CurrentSize = 2, + {StateName, QueueLength, Overflow, CurrentSize} = poolboy:status(Pool) + end(), + + ok = poolboy:stop(Pool) + end(), + + fun() -> + MaxOverflow = Size, + {ok, Pool} = new_pool(0, MaxOverflow), + fun() -> + StateName = overflow, + QueueLength = 0, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + poolboy:checkout(Pool), + + fun() -> + StateName = overflow, + QueueLength = 0, + Overflow = 1, + CurrentSize = 1, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + poolboy:checkout(Pool), + + fun() -> + StateName = overflow, + QueueLength = 0, + Overflow = 2, + CurrentSize = 2, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + ok = poolboy:stop(Pool) + end(), + + fun() -> + {ok, Pool} = new_pool(0, 0), + fun() -> + StateName = full, + QueueLength = 0, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = + poolboy:status(Pool) + end(), + + ok = poolboy:stop(Pool) + end(), + + true + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_demonitors_previously_waiting_processes +%% @doc Pool demonitors previously waiting processes +prop_demonitors_previously_waiting_processes() -> + ?FORALL(Size, range(5,100), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + + Self = self(), + Pid = spawn(fun() -> + W = poolboy:checkout(Pool), + Self ! ok, + timer:sleep(500), + poolboy:checkin(Pool, W), + receive ok -> ok end + end), + receive ok -> ok end, + Worker = poolboy:checkout(Pool), + fun() -> + MonitorCount = length(get_monitors(Pool)), + MonitorCount = 2 + end(), + poolboy:checkin(Pool, Worker), + timer:sleep(500), + fun() -> + MonitorCount = length(get_monitors(Pool)), + MonitorCount = 0 + end(), + Pid ! ok, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_demonitors_when_checkout_cancelled +%% @doc Pool demonitors when a checkout is cancelled +prop_demonitors_when_checkout_cancelled() -> + ?FORALL(Size, range(5,100), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + + Self = self(), + Pid = spawn(fun() -> + poolboy:checkout(Pool), + _ = (catch poolboy:checkout(Pool, true, 1000)), + Self ! ok, + receive ok -> ok end + end), + + timer:sleep(500), + + fun() -> + MonitorCount = length(get_monitors(Pool)), + MonitorCount = 2 + end(), + + receive ok -> ok end, + fun() -> + MonitorCount = length(get_monitors(Pool)), + MonitorCount = 2 + end(), + Pid ! ok, + + fun() -> + MonitorCount = length(get_monitors(Pool)), + %io:format("MonitorCount=~p, Size=~p~n",[MonitorCount,Size]) + MonitorCount = 0 + end(), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_default_strategy_lifo +%% @doc Check that LIFO is the default strategy +prop_default_strategy_lifo() -> + ?FORALL(Size, range(5,100), + begin + %% Default strategy is LIFO + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + Worker = poolboy:checkout(Pool), + ok = poolboy:checkin(Pool, Worker), + Worker = poolboy:checkout(Pool), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_lifo_strategy +%% @doc Check LIFO strategy +prop_lifo_strategy() -> + ?FORALL(Size, range(5,100), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow, lifo), + Worker = poolboy:checkout(Pool), + ok = poolboy:checkin(Pool, Worker), + Worker = poolboy:checkout(Pool), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_fifo_strategy +%% @doc Check FIFO strategy +prop_fifo_strategy() -> + ?FORALL(Size, range(2,100), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow, fifo), + Worker1 = poolboy:checkout(Pool), + ok = poolboy:checkin(Pool, Worker1), + Worker2 = poolboy:checkout(Pool), + true = Worker1 =/= Worker2, + + Workers = [poolboy:checkout(Pool) || _ <- lists:seq(0, Size-2)], + Worker1 = hd(lists:reverse(Workers)), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_reuses_waiting_monitor_on_worker_exit +%% @doc Pool reuses waiting monitor when a worker exits +prop_reuses_waiting_monitor_on_worker_exit() -> + ?FORALL(Size, range(1,100), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + Self = self(), + Pid = spawn(fun() -> + Worker = poolboy:checkout(Pool), + Self ! {worker, Worker}, + poolboy:checkout(Pool), + receive ok -> ok end + end), + Worker = receive {worker, Worker1} -> Worker1 end, + Ref = monitor(process, Worker), + exit(Worker, kill), + receive + {'DOWN', Ref, _, _, _} -> + ok + end, + MonitorCount = length(get_monitors(Pool)), + %io:format("MonitorCount=~p, Size=~p~n",[MonitorCount,Size]) + MonitorCount = 1, + Pid ! ok, + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_transaction_timeout_without_exit +%% @doc Recover from timeout without exit handling +prop_transaction_timeout_without_exit() -> + ?FORALL(Size, range(1,100), + %?FORALL(_Bool, boolean(), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + fun() -> + StateName = ready, + QueueLength = Size, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = pool_call(Pool, status) + end(), + WorkerList = pool_call(Pool, get_all_workers), + true = is_list(WorkerList), + true = 0 < length(WorkerList), + + %io:format("WorkerList=~p~n",[WorkerList]), + spawn(poolboy, transaction, [Pool, + fun(Worker) -> + ok = pool_call(Worker, work) + end, + 0]), + timer:sleep(100), + WorkerList = pool_call(Pool, get_all_workers), + fun() -> + StateName = ready, + QueueLength = Size, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = pool_call(Pool, status) + end(), + + ok == poolboy:stop(Pool) + end + ). + +%% shell command for a test: rebar3 proper -n 1 -p prop_transaction_timeout +%% @doc Recover from transaction timeout +prop_transaction_timeout() -> + ?FORALL(Size, range(1,100), + %?FORALL(_Bool, boolean(), + begin + MaxOverflow = 0, + {ok, Pool} = new_pool(Size, MaxOverflow), + fun() -> + StateName = ready, + QueueLength = Size, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = pool_call(Pool, status) + end(), + WorkerList = pool_call(Pool, get_all_workers), + true = is_list(WorkerList), + true = 0 < length(WorkerList), + + ok = try + poolboy:transaction(Pool, + fun(Worker1) -> + ok = pool_call(Worker1, work) + end, + 0) + catch exit:{timeout,_} -> + ok + end, + + + WorkerList = pool_call(Pool, get_all_workers), + fun() -> + StateName = ready, + QueueLength = Size, + Overflow = 0, + CurrentSize = 0, + {StateName, QueueLength, Overflow, CurrentSize} = pool_call(Pool, status) + end(), + + ok == poolboy:stop(Pool) + end + ). + +%%%%%%%%%%%%%%% +%%% Helpers %%% +%%%%%%%%%%%%%%% +new_pool(Size, MaxOverflow) -> + poolboy:start_link([{name, {local, poolboy_test}}, + {worker_module, poolboy_test_worker}, + {size, Size}, {max_overflow, MaxOverflow}]). + + + + +new_pool(Size, MaxOverflow, Strategy) -> + poolboy:start_link([{name, {local, poolboy_test}}, + {worker_module, poolboy_test_worker}, + {size, Size}, {max_overflow, MaxOverflow}, + {strategy, Strategy}]). + +pool_call(ServerRef, Request) -> + gen_server:call(ServerRef, Request). + +checkin_worker(Pool, Worker) -> + %% There's no easy way to wait for a checkin to complete, because it's + %% async and the supervisor may kill the process if it was an overflow + %% worker. The only solution seems to be a nasty hardcoded sleep. + poolboy:checkin(Pool, Worker), + timer:sleep(500). + +%% Tell a worker to exit and await its impending doom. +kill_worker(Pid) -> + erlang:monitor(process, Pid), + pool_call(Pid, die), + receive + {'DOWN', _, process, Pid, _} -> + ok + end. + +get_monitors(Pid) -> + %% Synchronise with the Pid to ensure it has handled all expected work. + _ = sys:get_status(Pid), + [{monitors, Monitors}] = erlang:process_info(Pid, [monitors]), + Monitors. + +%%%%%%%%%%%%%%%%%% +%%% Generators %%% +%%%%%%%%%%%%%%%%%% +