Current section
Files
Jump to
Current section
Files
src/locker_proper.erl
-module(locker_proper).
-compile([export_all]).
-include_lib("proper/include/proper.hrl").
-record(state, {master_leases, replicated_leases}).
-define(MASTERS, [host_name("a")]).
-define(REPLICAS, [host_name("b")]).
test() ->
proper:quickcheck(prop_lock_release()).
prop_lock_release() ->
?FORALL(Commands, parallel_commands(?MODULE),
?TRAPEXIT(
begin
[A, B] = Cluster = setup([a, b]),
ok = rpc:call(A, locker, set_nodes, [Cluster, [A], [B]]),
{Seq, P, Result} = run_parallel_commands(?MODULE, Commands),
teardown(Cluster),
?WHENFAIL(
io:format("Sequential: ~p\nParallel: ~p\nRes: ~p\n",
[Seq, P, Result]),
Result =:= ok)
end)).
key() ->
elements([1]).
value() ->
elements([foo, bar]).
get_master() ->
elements(?MASTERS).
get_replica() ->
elements(?REPLICAS).
get_node() ->
elements(?MASTERS ++ ?REPLICAS).
is_master(N) ->
lists:member(N, ?MASTERS).
is_replica(N) ->
lists:member(N, ?REPLICAS).
command(S) ->
Leases = S#state.master_leases =/= [],
oneof([{call, ?MODULE, lock, [get_node(), key(), value()]}] ++
[{call, ?MODULE, read, [get_node(), key()]}] ++
[?LET({Key, Value}, elements(S#state.master_leases),
{call, ?MODULE, release,
[get_node(), Key, Value]}) || Leases] ++
[{call, ?MODULE, update, [get_node(), key(), value(), value()]}
|| Leases] ++
[{call, ?MODULE, replicate, []}]
).
lock(Node, Key, Value) ->
rpc:call(Node, locker, lock, [Key, Value]).
release(Node, Key, Value) ->
rpc:call(Node, locker, release, [Key, Value]).
update(Node, Key, Value, NewValue) ->
rpc:call(Node, locker, update, [Key, Value, NewValue]).
replicate() ->
rpc:sbcast(?MASTERS, locker, push_trans_log).
read(Node, Key) ->
rpc:call(Node, locker, dirty_read, [Key]).
initial_state() ->
#state{master_leases = [], replicated_leases = []}.
precondition(S, {call, _, release, [_, Key, _Value]}) ->
lists:keymember(Key, 1, S#state.master_leases);
precondition(_, _) ->
true.
next_state(S, _V, {call, _, lock, [_, Key, Value]}) ->
case lists:keymember(Key, 1, S#state.master_leases) of
true ->
S;
false ->
S#state{master_leases = [{Key, Value} | S#state.master_leases]}
end;
next_state(S, _V, {call, _, release, [_, Key, Value]}) ->
case lists:member({Key, Value}, S#state.master_leases) of
true ->
S#state{master_leases = lists:delete({Key, Value},
S#state.master_leases),
replicated_leases =
lists:delete({Key, Value}, S#state.replicated_leases)};
false ->
S
end;
next_state(S, _V, {call, _, update, [_, Key, Value, NewValue]}) ->
case lists:member({Key, Value}, S#state.master_leases) of
true ->
S#state{master_leases = [{Key, NewValue} |
lists:delete({Key, Value},
S#state.master_leases)]};
false ->
S
end;
next_state(S, _V, {call, _, replicate, []}) ->
S#state{replicated_leases = S#state.master_leases};
next_state(S, _V, {call, _, read, _}) ->
S.
postcondition(S, {call, _, lock, [_, Key, _Value]}, Result) ->
case Result of
{ok, _, _, _} ->
not lists:keymember(Key, 1, S#state.master_leases);
{error, no_quorum} ->
lists:keymember(Key, 1, S#state.master_leases)
end;
postcondition(S, {call, _, release, [_, Key, Value]}, {ok, _, _, _}) ->
lists:member({Key, Value}, S#state.master_leases);
postcondition(S, {call, _, release, [_, Key, _Value]}, {error, no_quorum}) ->
lists:keymember(Key, 1, S#state.master_leases);
postcondition(S, {call, _, update, [_, Key, Value, _NewValue]},
{ok, _, _, _}) ->
lists:member({Key, Value}, S#state.master_leases);
postcondition(S, {call, _, update, [_, Key, Value, _NewValue]},
{error, no_quorum}) ->
Val = lists:keymember(Key, 1, S#state.master_leases),
Val orelse (Val =/= Value);
postcondition(_S, {call, _, replicate, []}, _) ->
true;
postcondition(S, {call, _, read, [Node, Key]}, Result) ->
case is_master(Node) of
true ->
case Result of
{ok, Value} ->
lists:member({Key, Value}, S#state.master_leases);
{error, not_found} ->
not lists:keymember(Key, 1, S#state.master_leases)
end;
false ->
case Result of
{ok, Value} ->
lists:member({Key, Value}, S#state.replicated_leases);
{error, not_found} ->
not lists:keymember(Key, 1, S#state.replicated_leases)
end
end.
%%
%% SETUP
%%
setup(Name) when is_atom(Name) ->
{ok, Node} = slave:start_link(list_to_atom(net_adm:localhost()), Name),
true = rpc:call(Node, code, add_path, ["ebin"]),
{ok, _} = rpc:call(Node, locker, start_link, [1]),
{ok, _, _, R1, R2, R3} = rpc:call(Node, locker, get_debug_state, []),
{ok, cancel} = rpc:call(Node, timer, cancel, [R1]),
{ok, cancel} = rpc:call(Node, timer, cancel, [R2]),
{ok, cancel} = rpc:call(Node, timer, cancel, [R3]),
Node;
setup(NodeNames) ->
lists:map(fun setup/1, NodeNames).
teardown(Nodes) ->
lists:map(fun slave:stop/1, Nodes).
%% @doc Return fully qualified name for local host node.
host_name(Name) ->
list_to_atom(Name ++ "@" ++ net_adm:localhost()).