Skip to content

Commit 94e60be

Browse files
committed
Use dotted version vectors for causality tracking when running tests
1 parent 272ec03 commit 94e60be

File tree

4 files changed

+201
-64
lines changed

4 files changed

+201
-64
lines changed

rebar.config

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
]},
2323
{docs, [
2424
{deps, [edown]}
25+
]},
26+
{test, [
27+
{deps, [
28+
{dvv, ".*",
29+
{git, "https://github.com/ricardobcl/Dotted-Version-Vectors.git", {tag, "1.0"}}}
30+
]}
2531
]}
2632
]}.
2733

test/plumtree_SUITE.erl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ membership_test(Config) ->
161161
lists:foreach(fun(_) ->
162162
{_, Node} = plumtree_test_utils:select_random(Nodes),
163163
ok = rpc:call(Node,
164-
plumtree_broadcast, broadcast,
165-
[{k, rand_compat:uniform()}, plumtree_test_broadcast_handler])
164+
plumtree_test_broadcast_handler, put,
165+
[k, rand_compat:uniform()])
166166
end, lists:seq(1, BroadcastRounds1)),
167167
%% allow 100ms per broadcast to settle
168168
timer:sleep(100 * BroadcastRounds1),
@@ -177,8 +177,8 @@ membership_test(Config) ->
177177
lists:foreach(fun(_) ->
178178
{_, Node} = plumtree_test_utils:select_random(Nodes),
179179
ok = rpc:call(Node,
180-
plumtree_broadcast, broadcast,
181-
[{k, rand_compat:uniform()}, plumtree_test_broadcast_handler])
180+
plumtree_test_broadcast_handler, put,
181+
[k, rand_compat:uniform()])
182182
end, lists:seq(1, BroadcastRounds2)),
183183
%% allow 100ms per broadcast to settle
184184
timer:sleep(100 * BroadcastRounds1),
@@ -225,8 +225,8 @@ broadcast_test(Config) ->
225225
lists:foreach(fun(_) ->
226226
{_, Node} = plumtree_test_utils:select_random(Nodes),
227227
ok = rpc:call(Node,
228-
plumtree_broadcast, broadcast,
229-
[{k, rand_compat:uniform()}, plumtree_test_broadcast_handler])
228+
plumtree_test_broadcast_handler, put,
229+
[k, rand_compat:uniform()])
230230
end, lists:seq(1, BroadcastRounds1)),
231231
%% allow 500ms per broadcast to settle
232232
timer:sleep(200 * BroadcastRounds1),
@@ -238,13 +238,13 @@ broadcast_test(Config) ->
238238
Rand = rand_compat:uniform(),
239239
{_, RandomNode} = plumtree_test_utils:select_random(Nodes),
240240
ok = rpc:call(RandomNode,
241-
plumtree_broadcast, broadcast,
242-
[{k, Rand}, plumtree_test_broadcast_handler]),
241+
plumtree_test_broadcast_handler, put,
242+
[k, Rand]),
243243
ct:pal("requested node ~p to broadcast {k, ~p}",
244244
[RandomNode, Rand]),
245245

246246
VerifyFun = fun(Node, Rand0) ->
247-
case rpc:call(Node, plumtree_test_broadcast_handler, read, [k]) of
247+
case rpc:call(Node, plumtree_test_broadcast_handler, get, [k]) of
248248
{error, not_found} ->
249249
{false, not_found};
250250
{ok, NodeRand} when NodeRand =:= Rand0 -> true;

test/plumtree_test_broadcast_handler.erl

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
is_stale/1,
3131
graft/1,
3232
exchange/1]).
33+
3334
%% gen_server callbacks
3435
-export([init/1,
3536
handle_call/3,
@@ -40,40 +41,42 @@
4041

4142
%% API
4243
-export([start_link/0,
43-
read/1]).
44+
get/1,
45+
put/2]).
4446

4547
-record(state, {}).
46-
-type state() :: #state{}.
48+
-type state() :: #state{}.
4749

4850
-spec start_link() -> ok.
4951
start_link() ->
5052
{ok, _} = gen_server:start_link({local, ?SERVER}, ?MODULE,
5153
[], []),
5254
ok.
5355

54-
-spec read(Key :: any()) -> {ok, any()} | {error, not_found}.
55-
read(Key) ->
56-
case ets:lookup(?MODULE, Key) of
57-
[{Key, Value}] ->
58-
% lager:info("read key ~p: ~p",
59-
% [Key, Value]),
60-
{ok, Value};
61-
_ ->
62-
lager:info("unable to find key: ~p",
63-
[Key]),
64-
{error, not_found}
56+
-spec get(Key :: any()) -> {error, not_found} | {ok, any()}.
57+
get(Key) ->
58+
case dbread(Key) of
59+
undefined -> {error, not_found};
60+
Obj ->
61+
{ok, plumtree_test_object:value(Obj)}
6562
end.
6663

64+
-spec put(Key :: any(),
65+
Value :: any()) -> ok.
66+
put(Key, Value) ->
67+
Existing = dbread(Key),
68+
UpdatedObj = plumtree_test_object:modify(Existing, Value, this_server_id()),
69+
dbwrite(Key, UpdatedObj),
70+
plumtree_broadcast:broadcast({Key, UpdatedObj}, plumtree_test_broadcast_handler),
71+
ok.
72+
6773
%%%===================================================================
6874
%%% gen_server callbacks
6975
%%%===================================================================
7076

7177
%% @private
7278
-spec init([[any()], ...]) -> {ok, state()}.
7379
init([]) ->
74-
msgs_seen = ets:new(msgs_seen, [named_table, set, public,
75-
{keypos, 1},
76-
{read_concurrency, true}]),
7780
?MODULE = ets:new(?MODULE, [named_table, set, public,
7881
{keypos, 1},
7982
{read_concurrency, true}]),
@@ -111,61 +114,50 @@ code_change(_OldVsn, State, _Extra) ->
111114

112115
%% Return a two-tuple of message id and payload from a given broadcast
113116
-spec broadcast_data(any()) -> {any(), any()}.
114-
broadcast_data({Key, _Value} = Data) ->
115-
MsgId = erlang:phash2(Data),
117+
broadcast_data({Key, Object}) ->
118+
MsgId = {Key, plumtree_test_object:context(Object)},
116119
lager:info("broadcast_data(~p), msg id: ~p",
117-
[Data, MsgId]),
118-
true = ets:insert(msgs_seen, {MsgId, Key}),
119-
true = ets:insert(?MODULE, Data),
120-
{MsgId, Data}.
120+
[Object, MsgId]),
121+
{MsgId, Object}.
121122

122123
%% Given the message id and payload, merge the message in the local state.
123124
%% If the message has already been received return `false', otherwise return `true'
124125
-spec merge(any(), any()) -> boolean().
125-
merge(MsgId, {Key, _Value} = Payload) ->
126-
case ets:lookup(msgs_seen, MsgId) of
127-
[{MsgId, _}] ->
128-
lager:info("msg with id ~p has already been seen",
129-
[MsgId]),
130-
false;
131-
_ ->
132-
lager:info("merging(~p, ~p) in local state",
133-
[MsgId, Payload]),
134-
%% insert the message in the local state
135-
true = ets:insert(?MODULE, Payload),
136-
%% mark this message as been seen
137-
true = ets:insert_new(msgs_seen, {MsgId, Key}),
126+
merge({Key, _Context} = MsgId, RemoteObj) ->
127+
lager:info("merge msg id ~p, object: ~p",
128+
[MsgId, RemoteObj]),
129+
Existing = dbread(Key),
130+
case plumtree_test_object:reconcile(RemoteObj, Existing) of
131+
false -> false;
132+
{true, Reconciled} ->
133+
dbwrite(Key, Reconciled),
138134
true
139135
end.
140136

141137
%% Return true if the message (given the message id) has already been received.
142138
%% `false' otherwise
143139
-spec is_stale(any()) -> boolean().
144-
is_stale(MsgId) ->
145-
case ets:lookup(msgs_seen, MsgId) of
146-
[{MsgId, _}] ->
147-
lager:info("is_stale(~p): ~p",
148-
[MsgId, true]),
149-
true;
150-
_ ->
151-
lager:info("is_stale(~p): ~p",
152-
[MsgId, false]),
153-
false
154-
end.
140+
is_stale({Key, Context}) ->
141+
Existing = dbread(Key),
142+
plumtree_test_object:is_stale(Context, Existing).
155143

156144
%% Return the message associated with the given message id. In some cases a message
157145
%% has already been sent with information that subsumes the message associated with the given
158146
%% message id. In this case, `stale' is returned.
159147
-spec graft(any()) -> stale | {ok, any()} | {error, any()}.
160-
graft(MsgId) ->
161-
% lager:info("graft(~p)",
162-
% [MsgId]),
163-
case ets:lookup(msgs_seen, MsgId) of
164-
[{MsgId, Key}] ->
165-
[{Key,Msg}] = ets:lookup(?MODULE, Key),
166-
{ok, {Key, Msg}};
167-
_ ->
168-
{error, not_found}
148+
graft({Key, Context}) ->
149+
case dbread(Key) of
150+
undefined ->
151+
%% this *really* should not happen
152+
lager:alert("unable to graft key ~p, could not find it",
153+
[Key]),
154+
{error, not_found};
155+
Object ->
156+
LocalContext = plumtree_test_object:context(Object),
157+
case LocalContext =:= Context of
158+
true -> {ok, Object};
159+
false -> stale
160+
end
169161
end.
170162

171163
%% Trigger an exchange between the local handler and the handler on the given node.
@@ -176,3 +168,24 @@ graft(MsgId) ->
176168
-spec exchange(node()) -> {ok, pid()} | {error, term()}.
177169
exchange(_Node) ->
178170
{ok, self()}.
171+
172+
%% @private
173+
-spec dbread(Key :: any()) -> any() | undefined.
174+
dbread(Key) ->
175+
case ets:lookup(?MODULE, Key) of
176+
[{Key, Object}] ->
177+
Object;
178+
_ ->
179+
undefined
180+
end.
181+
182+
%% @private
183+
-spec dbwrite(Key :: any(),
184+
Value :: any()) -> any().
185+
dbwrite(Key, Object) ->
186+
ets:insert(?MODULE, {Key, Object}),
187+
Object.
188+
189+
%% @private
190+
this_server_id() -> node().
191+

test/plumtree_test_object.erl

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
%% -------------------------------------------------------------------
2+
%%
3+
%% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved.
4+
%%
5+
%% This file is provided to you under the Apache License,
6+
%% Version 2.0 (the "License"); you may not use this file
7+
%% except in compliance with the License. You may obtain
8+
%% a copy of the License at
9+
%%
10+
%% http://www.apache.org/licenses/LICENSE-2.0
11+
%%
12+
%% Unless required by applicable law or agreed to in writing,
13+
%% software distributed under the License is distributed on an
14+
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
%% KIND, either express or implied. See the License for the
16+
%% specific language governing permissions and limitations
17+
%% under the License.
18+
%%
19+
%% -------------------------------------------------------------------
20+
-module(plumtree_test_object).
21+
22+
%% API
23+
-export([context/1,
24+
value/1,
25+
values/1,
26+
modify/3,
27+
modify/4,
28+
is_stale/2,
29+
reconcile/2]).
30+
31+
-type test_object() :: dvvset:clock().
32+
-type test_context() :: dvvset:vector().
33+
-type test_value() :: any().
34+
35+
%% @doc returns a single value. if the object holds more than one value an error is generated
36+
%% @see values/2
37+
-spec value(test_object()) -> test_value().
38+
value(Object) ->
39+
[Value] = values(Object),
40+
Value.
41+
42+
%% @doc returns a list of values held in the object
43+
-spec values(test_object()) -> [test_value()].
44+
values(Object) ->
45+
[Value || {Value, _Ts} <- dvvset:values(Object)].
46+
47+
%% @doc returns the context (opaque causal history) for the given object
48+
-spec context(test_object()) -> test_context().
49+
context(Object) ->
50+
dvvset:join(Object).
51+
52+
%% @doc modifies a potentially existing object, setting its value and updating
53+
%% the causual history.
54+
-spec modify(test_object() | undefined,
55+
test_value(),
56+
term()) -> test_object().
57+
modify(undefined, Value, ServerId) ->
58+
modify(undefined, undefined, Value, ServerId);
59+
modify(Object, Value, ServerId) ->
60+
modify(Object, context(Object), Value, ServerId).
61+
62+
-spec modify(test_object() | undefined,
63+
test_context(),
64+
test_value(),
65+
term()) -> test_object().
66+
modify(undefined, _Context, Value, ServerId) ->
67+
%% Ignore the context since we dont have a value, its invalid if not
68+
%% empty anyways, so give it a valid one
69+
NewRecord = dvvset:new(timestamped_value(Value)),
70+
dvvset:update(NewRecord, ServerId);
71+
modify(Existing, Context, Value, ServerId) ->
72+
InsertRec = dvvset:new(Context, timestamped_value(Value)),
73+
dvvset:update(InsertRec, Existing, ServerId).
74+
75+
%% @doc Determines if the given context (version vector) is causually newer than
76+
%% an existing object. If the object missing or if the context does not represent
77+
%% an ancestor of the current key, false is returned. Otherwise, when the context
78+
%% does represent an ancestor of the existing object or the existing object itself,
79+
%% true is returned
80+
%% @private
81+
is_stale(RemoteContext, Obj) ->
82+
LocalContext = context(Obj),
83+
%% returns true (stale) when local context is causally newer or equal to remote context
84+
descends(LocalContext, RemoteContext).
85+
86+
%% @doc Reconciles a remote object received during replication or anti-entropy
87+
%% with a local object. If the remote object is an anscestor of or is equal to the local one
88+
%% `false' is returned, otherwise the reconciled object is returned as the second
89+
%% element of the two-tuple
90+
reconcile(RemoteObj, undefined) ->
91+
{true, RemoteObj};
92+
reconcile(undefined, _LocalObj) ->
93+
false;
94+
reconcile(RemoteObj, LocalObj) ->
95+
Less = dvvset:less(RemoteObj, LocalObj),
96+
Equal = dvvset:equal(RemoteObj, LocalObj),
97+
case not (Equal or Less) of
98+
false -> false;
99+
true ->
100+
LWW = fun ({_,TS1}, {_,TS2}) -> TS1 =< TS2 end,
101+
{true, dvvset:lww(LWW, dvvset:sync([LocalObj, RemoteObj]))}
102+
end.
103+
104+
%% @private
105+
descends(_, []) ->
106+
true;
107+
descends(Ca, Cb) ->
108+
[{NodeB, CtrB} | RestB] = Cb,
109+
case lists:keyfind(NodeB, 1, Ca) of
110+
false -> false;
111+
{_, CtrA} ->
112+
(CtrA >= CtrB) andalso descends(Ca, RestB)
113+
end.
114+
115+
%% @private
116+
timestamped_value(Value) ->
117+
{Value, os:timestamp()}.
118+

0 commit comments

Comments
 (0)