Skip to content

Commit

Permalink
Update for OTP 22
Browse files Browse the repository at this point in the history
  • Loading branch information
martinsumner committed Apr 21, 2020
1 parent b6218ae commit fb8705f
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
.test
.eunit
.dialyzer_plt
_build/*
rebar.lock
.DS_Store
12 changes: 3 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
REBAR = ./rebar
DIALYZER = dialyzer
REBAR = ./rebar3

DIALYZER_WARNINGS = -Wunmatched_returns -Werror_handling \
-Wrace_conditions -Wunderspecs
Expand All @@ -12,7 +11,7 @@ compile:
@$(REBAR) compile

test: compile
@$(REBAR) eunit skip_deps=true
@$(REBAR) do eunit skip_deps=true, cover

qc: compile
@$(REBAR) qc skip_deps=true
Expand All @@ -23,10 +22,5 @@ clean:
get-deps:
@$(REBAR) get-deps

build-plt:
@$(DIALYZER) --build_plt --output_plt .dialyzer_plt \
--apps kernel stdlib

dialyze: compile
@$(DIALYZER) --src src --plt .dialyzer_plt $(DIALYZER_WARNINGS) | \
fgrep -vf .dialyzer-ignore-warnings
@$(REBAR) dialyzer
Binary file removed rebar
Binary file not shown.
3 changes: 3 additions & 0 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
{platform_define, "^[0-9]+", namespaced_types}]}.
{eunit_opts, [verbose]}.
{cover_enabled, true}.
{xref_checks,[undefined_function_calls,undefined_functions,locals_not_used]}.
{profiles,
[{test, [{erl_opts, [nowarn_export_all]}]}]}.
Binary file added rebar3
Binary file not shown.
20 changes: 14 additions & 6 deletions src/poolboy.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
-module(poolboy).
-behaviour(gen_fsm).

-compile({nowarn_deprecated_function,
[{gen_fsm, start_link, 3},
{gen_fsm, start, 3},
{gen_fsm, reply, 2},
{gen_fsm, sync_send_event, 3},
{gen_fsm, send_event, 2},
{gen_fsm, sync_send_all_state_event, 2}]}).

-export([checkout/1, checkout/2, checkout/3, checkin/2, transaction/2,
child_spec/2, child_spec/3, start/1, start/2, start_link/1,
start_link/2, stop/1, status/1]).
Expand All @@ -26,24 +34,24 @@
-endif.

-record(state, {
supervisor :: pid(),
workers :: poolboy_queue(),
supervisor :: pid() | undefined,
workers :: poolboy_queue() |undefined,
waiting :: poolboy_queue(),
monitors :: ets:tid(),
size = 5 :: non_neg_integer(),
overflow = 0 :: non_neg_integer(),
max_overflow = 10 :: non_neg_integer()
}).

-spec checkout(Pool :: node()) -> pid().
-spec checkout(Pool :: node() | pid()) -> pid().
checkout(Pool) ->
checkout(Pool, true).

-spec checkout(Pool :: node(), Block :: boolean()) -> pid() | full.
-spec checkout(Pool :: node() | pid(), Block :: boolean()) -> pid() | full.
checkout(Pool, Block) ->
checkout(Pool, Block, ?TIMEOUT).

-spec checkout(Pool :: node(), Block :: boolean(), Timeout :: timeout())
-spec checkout(Pool :: node() | pid(), Block :: boolean(), Timeout :: timeout())
-> pid() | full.
checkout(Pool, Block, Timeout) ->
gen_fsm:sync_send_event(Pool, {checkout, Block, Timeout}, Timeout).
Expand All @@ -52,7 +60,7 @@ checkout(Pool, Block, Timeout) ->
checkin(Pool, Worker) when is_pid(Worker) ->
gen_fsm:send_event(Pool, {checkin, Worker}).

-spec transaction(Pool :: node(), Fun :: fun((Worker :: pid()) -> any()))
-spec transaction(Pool :: node() | pid(), Fun :: fun((Worker :: pid()) -> any()))
-> any().
transaction(Pool, Fun) ->
Worker = poolboy:checkout(Pool),
Expand Down
Binary file removed test/kill_race_ce.eqc
Binary file not shown.
262 changes: 262 additions & 0 deletions test/poolboy_eqc.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
-module(poolboy_eqc).
-compile([poolboy_test_]). %%FIXME: Probably needs fix by adding missing functions.

-ifdef(TEST).
-ifdef(EQC).
-include_lib("eqc/include/eqc.hrl").
-include_lib("eqc/include/eqc_statem.hrl").

-include_lib("eunit/include/eunit.hrl").

poolboy_test_() ->
{timeout, 20,
fun() ->
?assert(eqc:quickcheck(eqc:testing_time(4,
poolboy_eqc:prop_sequential()))),
?assert(eqc:quickcheck(eqc:testing_time(4,
poolboy_eqc:prop_parallel())))
end
}.

-record(state,
{
pid,
size,
max_overflow,
checked_out = []
}).

initial_state() ->
#state{}.

command(S) ->
oneof(
[{call, ?MODULE, start_poolboy, make_args(S, nat(), nat())} || S#state.pid == undefined] ++
[{call, ?MODULE, stop_poolboy, [S#state.pid]} || S#state.pid /= undefined] ++
[{call, ?MODULE, checkout_nonblock, [S#state.pid]} || S#state.pid /= undefined] ++
%% checkout shrinks to checkout_nonblock so we can simplify counterexamples
[{call, ?MODULE, ?SHRINK(checkout_block, [checkout_nonblock]), [S#state.pid]} || S#state.pid /= undefined] ++
[{call, ?MODULE, checkin, [S#state.pid, fault({call, ?MODULE, spawn_process, []}, elements(S#state.checked_out))]} || S#state.pid /= undefined, S#state.checked_out /= []] ++
[{call, ?MODULE, kill_worker, [elements(S#state.checked_out)]} || S#state.pid /= undefined, S#state.checked_out /= []] ++
[{call, ?MODULE, kill_idle_worker, [S#state.pid]} || S#state.pid /= undefined] ++
[{call, ?MODULE, spurious_exit, [S#state.pid]} || S#state.pid /= undefined]
).

make_args(_S, Size, Overflow) ->
[[{size, Size}, {max_overflow, Overflow}, {worker_module, poolboy_test_worker}, {name, {local, poolboy_eqc}}]].

spawn_process() ->
{spawn(fun() ->
timer:sleep(5000)
end), self()}.

spawn_linked_process(Pool) ->
Parent = self(),
Pid = spawn(fun() ->
link(Pool),
Parent ! {linked, self()},
timer:sleep(5000)
end),
receive
{linked, Pid} ->
Pid
end.

start_poolboy(Args) ->
{ok, Pid} = poolboy:start_link(Args),
Pid.

stop_poolboy(Pid) ->
gen_fsm:sync_send_all_state_event(Pid, stop),
timer:sleep(1).

checkout_nonblock(Pool) ->
{poolboy:checkout(Pool, false), self()}.

checkout_block(Pool) ->
{catch(poolboy:checkout(Pool, true, 100)), self()}.

checkin(Pool, {Worker, _}) ->
Res = poolboy:checkin(Pool, Worker),
gen_fsm:sync_send_all_state_event(Pool, get_avail_workers),
Res.

kill_worker({Worker, _}) ->
exit(Worker, kill),
timer:sleep(1),
Worker.

kill_idle_worker(Pool) ->
Pid = poolboy:checkout(Pool, false),
case Pid of
_ when is_pid(Pid) ->
poolboy:checkin(Pool, Pid),
kill_worker({Pid, self()});
_ ->
timer:sleep(1),
kill_idle_worker(Pool)
end.

spurious_exit(Pool) ->
Pid = spawn_linked_process(Pool),
exit(Pid, kill).

precondition(S,{call,_,start_poolboy,_}) ->
%% only start new pool when old one is stopped
S#state.pid == undefined;
precondition(S,_) when S#state.pid == undefined ->
%% all other states need a running pool
false;
precondition(S, {call, _, kill_worker, [Pid]}) ->
lists:member(Pid, S#state.checked_out);
precondition(S,{call,_,kill_idle_worker,[_Pool]}) ->
length(S#state.checked_out) < S#state.size;
precondition(S,{call,_,checkin,[_Pool, Pid]}) ->
lists:member(Pid, S#state.checked_out);
precondition(_S,{call,_,_,_}) ->
true.

%% check model state against internal state, only used in sequential tests
invariant(S = #state{pid=Pid},_) when Pid /= undefined ->
State = if length(S#state.checked_out) == S#state.size + S#state.max_overflow ->
full;
length(S#state.checked_out) >= S#state.size ->
overflow;
true ->
ready
end,

Workers = max(0, S#state.size - length(S#state.checked_out)),
OverFlow = max(0, length(S#state.checked_out) - S#state.size),
Monitors = length(S#state.checked_out),

RealStatus = gen_fsm:sync_send_all_state_event(Pid, status),
case RealStatus == {State, Workers, OverFlow, Monitors} of
true ->
true;
_ ->
{wrong_state, RealStatus, {State, Workers, OverFlow, Monitors}}
end;
invariant(_,_) ->
true.

%% what states block
blocking(S, {call, _, checkout_block, _}) ->
%% blocking checkout can block if we expect a checkout to fail
not checkout_ok(S);
blocking(_, _) ->
false.

postcondition(S,{call,_,checkout_block,[_Pool]},R) ->
case R of
{{'EXIT', {timeout, _}}, _} ->
case length(S#state.checked_out) >= S#state.size + S#state.max_overflow of
true ->
true;
_ ->
{checkout_block, R}
end;
_ ->
case length(S#state.checked_out) < S#state.size + S#state.max_overflow of
true ->
true;
_ ->
{checkout_block, R}
end
end;
postcondition(S,{call,_,checkout_nonblock,[_Pool]},R) ->
case R of
{full, _} ->
case length(S#state.checked_out) >= S#state.size + S#state.max_overflow of
true ->
true;
_ ->
{checkout_nonblock, R}
end;
_ ->
case length(S#state.checked_out) < S#state.size + S#state.max_overflow of
true ->
true;
_ ->
{checkout_block, R}
end
end;
postcondition(_S, {call,_,checkin,_}, R) ->
case R of
ok ->
true;
_ ->
{checkin, R}
end;
postcondition(_S,{call,_,_,_},_R) ->
true.

next_state(S,V,{call,_,start_poolboy, [Args]}) ->
S#state{pid=V,
size=proplists:get_value(size, Args),
max_overflow=proplists:get_value(max_overflow, Args)
};
next_state(S,_V,{call,_,stop_poolboy, [_Args]}) ->
S#state{pid=undefined, checked_out=[]};
next_state(S,V,{call,_,checkout_block,_}) ->
%% if the model says the checkout worked, store the result
case checkout_ok(S) of
false ->
S;
_ ->
S#state{checked_out=S#state.checked_out++[V]}
end;
next_state(S,V,{call,_,checkout_nonblock,_}) ->
%% if the model says the checkout worked, store the result
case checkout_ok(S) of
false ->
S;
_ ->
S#state{checked_out=S#state.checked_out++[V]}
end;
next_state(S,_V,{call, _, checkin, [_Pool, Worker]}) ->
S#state{checked_out=S#state.checked_out -- [Worker]};
next_state(S,_V,{call, _, kill_worker, [Worker]}) ->
S#state{checked_out=S#state.checked_out -- [Worker]};
next_state(S,_V,{call, _, kill_idle_worker, [_Pool]}) ->
S;
next_state(S,_V,{call, _, spurious_exit, [_Pool]}) ->
S;
next_state(S,V,{call, erlang, self, []}) ->
%% added after test generation, values are never symbolic
S#state{checked_out=[{Worker, Pid} || {Worker, Pid} <- S#state.checked_out, Pid /= V]}.


prop_sequential() ->
fault_rate(1, 10,
?FORALL(Cmds,commands(?MODULE),
?TRAPEXIT(
aggregate(command_names(Cmds),
begin
{H,S,Res} = run_commands(?MODULE,Cmds),
catch(stop_poolboy(whereis(poolboy_eqc))),
?WHENFAIL(io:format("History: ~p\nState: ~p\nRes: ~p\n~p\n",
[H,S,Res, zip(Cmds, [Y || {_, Y} <- H])]),
Res == ok)
end)))).

prop_parallel() ->
fault_rate(1, 10,
?FORALL(Cmds={Seq,Par},parallel_commands(?MODULE),
?TRAPEXIT(
aggregate(command_names(Cmds),
begin
NewPar = [P ++ [{set, {var, 0}, {call, erlang, self, []}}] || P <- Par],
{H,S,Res} = run_parallel_commands(?MODULE,{Seq,NewPar}),
catch(stop_poolboy(whereis(poolboy_eqc))),
?WHENFAIL(io:format("History: ~p\nState: ~p\nRes: ~p\n",
[H,S,Res]),
Res == ok)
end)))).


checkout_ok(S) ->
length(S#state.checked_out) < S#state.size + S#state.max_overflow.

-endif.
-endif.
3 changes: 3 additions & 0 deletions test/poolboy_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

-include_lib("eunit/include/eunit.hrl").

-compile({nowarn_deprecated_function,
[{gen_fsm, sync_send_all_state_event, 2}]}).

-define(sync(Pid, Event),
gen_fsm:sync_send_all_state_event(Pid, Event)).

Expand Down
Binary file removed test/unknown2_ce.eqc
Binary file not shown.
Binary file removed test/unknown3_ce.eqc
Binary file not shown.
Binary file removed test/unknown_ce.eqc
Binary file not shown.

0 comments on commit fb8705f

Please sign in to comment.