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..5165567 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ compile: test: @$(REBAR) eunit + @$(REBAR) proper -n 1 qc: compile @$(REBAR) eqc @@ -18,3 +19,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/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 1d494ca..d5b4963 100644 --- a/rebar.config +++ b/rebar.config @@ -10,6 +10,22 @@ {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, [ + {edoc_opts, [ + {private, true} + ]} + ]} +]}. + +{edoc_opts, [ + {preprocess, true}, {stylesheet, "style.css"} +]}. + +{project_plugins, [rebar3_proper,rebar3_eqc]}. diff --git a/src/poolboy.erl b/src/poolboy.erl index db20541..e74e5b6 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,136 @@ 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. +%% @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 + 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 +331,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 +379,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 +472,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 +521,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 +558,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 +643,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 +665,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]}, 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 %%% +%%%%%%%%%%%%%%%%%% +