Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

WIP commit of counters-in-riak-object

  • Loading branch information...
commit d515125cca819e0103787573116c339f0f16afba 1 parent c2f92dd
Russell Brown russelldb authored
90 src/riak_kv_counter.erl
View
@@ -0,0 +1,90 @@
+%% -------------------------------------------------------------------
+%%
+%% riak_kv_counter: Counter logic to bridge riak_object and riak_kv_pncounter
+%%
+%% Copyright (c) 2007-2010 Basho Technologies, Inc. All Rights Reserved.
+%%
+%% This file is provided to you under the Apache License,
+%% Version 2.0 (the "License"); you may not use this file
+%% except in compliance with the License. You may obtain
+%% a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+%%
+%% -------------------------------------------------------------------
+-module(riak_kv_counter).
+
+-export([update/4, merge/2, value/1]).
+
+-include("riak_kv_wm_raw.hrl").
+
+%% A counter is a two tuple of a `riak_kv_pncounter' stored in a `riak_object'
+%% with the tag `riak_kv_pncounter' as the first element.
+%% Since counters can be stored with any name, in any bucket, there is a
+%% chance that some sibing value for a counter is
+%% not a `riak_kv_pncounter' in that case, we keep the sibling
+%% for later resolution by the user.
+-spec update(riak_object:riak_object(), term(), binary(), integer()) ->
+ riak_object:riak_object().
+update(RObj, IndexSpecs, Actor, Amt) ->
+ Values = riak_object:get_contents(RObj),
+ {Counter0, NonCounterSiblings} = merge_values(Values, riak_kv_pncounter:new(), []),
+ Meta = merged_meta(IndexSpecs),
+ Counter = update_counter(Counter0, Actor, Amt),
+ update_object(RObj, Meta, Counter, NonCounterSiblings).
+
+merge(RObj, IndexSpecs) ->
+ Values = riak_object:get_contents(RObj),
+ {Counter, NonCounterSiblings} = merge_values(Values, riak_kv_pncounter:new(), []),
+ Meta = merged_meta(IndexSpecs),
+ update_object(RObj, Meta, Counter, NonCounterSiblings).
+
+merge_values([], Mergedest, NonCounterSiblings) ->
+ {Mergedest, NonCounterSiblings};
+merge_values([{_MD, {riak_kv_pncounter, Value}} | Rest], Mergedest, NonCounterSiblings) ->
+ merge_values(Rest, do_merge(Value, Mergedest), NonCounterSiblings);
+merge_values([NotACounter|Rest], Mergedest, NonCounterSiblings) ->
+ merge_values(Rest, Mergedest, [NotACounter | NonCounterSiblings]).
+
+do_merge(C1, C2) ->
+ riak_kv_pncounter:merge(C1, C2).
+
+%% Only indexes are allowed in counter
+%% meta data.
+%% The job of merging index meta data has
+%% already been done to get the indexspecs
+%% therefore create a meta that is
+%% only the index meta data we already know about
+merged_meta(IndexSpecs) ->
+ Indexes = [{Index, Value} || {Op, Index, Value} <- IndexSpecs,
+ Op =:= add],
+ dict:store(?MD_INDEX, Indexes, dict:new()).
+
+update_counter(Counter, Actor, Amt) ->
+ Op = counter_op(Amt),
+ riak_kv_pncounter:update(Op, Actor, Counter).
+
+counter_op(Amt) when Amt < 0 ->
+ {decrement, Amt * -1};
+counter_op(Amt) ->
+ {increment, Amt}.
+
+update_object(RObj, Meta, Counter, []) ->
+ RObj2 = riak_object:update_value(RObj, {riak_kv_pncounter, Counter}),
+ RObj3 = riak_object:update_metadata(RObj2, Meta),
+ riak_object:apply_updates(RObj3);
+update_object(RObj, Meta, Counter, SiblingValues) ->
+ %% keep non-counter siblings, too
+ riak_object:set_contents(RObj, [{Meta, {riak_kv_pncounter, Counter}} | SiblingValues]).
+
+value(RObj) ->
+ Contents = riak_object:get_contents(RObj),
+ {Counter, _NonCounterSiblings} = merge_values(Contents, riak_kv_pncounter:new(), []),
+ riak_kv_pncounter:value(Counter).
187 src/riak_kv_gcounter.erl
View
@@ -0,0 +1,187 @@
+%% -------------------------------------------------------------------
+%%
+%% riak_kv_gcounter: A state based, grow only, convergent counter
+%%
+%% Copyright (c) 2007-2012 Basho Technologies, Inc. All Rights Reserved.
+%%
+%% This file is provided to you under the Apache License,
+%% Version 2.0 (the "License"); you may not use this file
+%% except in compliance with the License. You may obtain
+%% a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+%%
+%% -------------------------------------------------------------------
+
+%%% @doc
+%%% a G-Counter CRDT, borrows liberally from argv0 and Justin Sheehy's vclock module
+%%% @end
+
+-module(riak_kv_gcounter).
+
+-export([new/0, new/2, value/1, update/3, merge/2, equal/2]).
+
+-ifdef(EQC).
+-include_lib("eqc/include/eqc.hrl").
+-export([gen_op/0, update_expected/3, eqc_state_value/1]).
+-endif.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% EQC generator
+-ifdef(EQC).
+gen_op() ->
+ oneof([increment, {increment, gen_pos()}]).
+
+gen_pos()->
+ ?LET(X, int(), 1+abs(X)).
+
+update_expected(_ID, increment, Prev) ->
+ Prev+1;
+update_expected(_ID, {increment, By}, Prev) ->
+ Prev+By;
+update_expected(_ID, _Op, Prev) ->
+ Prev.
+
+eqc_state_value(S) ->
+ S.
+-endif.
+
+new() ->
+ [].
+
+%% create a counter with an initial update
+new(Id, Count) when is_integer(Count), Count > 0 ->
+ update({increment, Count}, Id, new()).
+
+value(GCnt) ->
+ lists:sum([ Cnt || {_Act, Cnt} <- GCnt]).
+
+update(increment, Actor, GCnt) ->
+ increment_by(1, Actor, GCnt);
+update({increment, Amount}, Actor, GCnt) when is_integer(Amount), Amount > 0 ->
+ increment_by(Amount, Actor, GCnt).
+
+merge(GCnt1, GCnt2) ->
+ merge(GCnt1, GCnt2, []).
+
+merge([], [], Acc) ->
+ lists:reverse(Acc);
+merge(LeftOver, [], Acc) ->
+ lists:reverse(Acc, LeftOver);
+merge([], LeftOver, Acc) ->
+ lists:reverse(Acc, LeftOver);
+merge([{Actor1, Cnt1}=AC1|Rest], Clock2, Acc) ->
+ case lists:keytake(Actor1, 1, Clock2) of
+ {value, {Actor1, Cnt2}, RestOfClock2} ->
+ merge(Rest, RestOfClock2, [{Actor1, max(Cnt1, Cnt2)}|Acc]);
+ false ->
+ merge(Rest, Clock2, [AC1|Acc])
+ end.
+
+equal(VA,VB) ->
+ lists:sort(VA) =:= lists:sort(VB).
+
+%% priv
+increment_by(Amount, Actor, GCnt) when is_integer(Amount), Amount > 0 ->
+ {Ctr, NewGCnt} = case lists:keytake(Actor, 1, GCnt) of
+ false ->
+ {Amount, GCnt};
+ {value, {_N, C}, ModGCnt} ->
+ {C + Amount, ModGCnt}
+ end,
+ [{Actor, Ctr}|NewGCnt].
+
+
+%% ===================================================================
+%% EUnit tests
+%% ===================================================================
+-ifdef(TEST).
+
+-ifdef(EQC).
+eqc_value_test_() ->
+ {timeout, 120, [?_assert(crdt_statem_eqc:prop_converge(0, 1000, ?MODULE))]}.
+-endif.
+
+new_test() ->
+ ?assertEqual([], new()).
+
+value_test() ->
+ GC1 = [{1, 1}, {2, 13}, {3, 1}],
+ GC2 = [],
+ ?assertEqual(15, value(GC1)),
+ ?assertEqual(0, value(GC2)).
+
+update_increment_test() ->
+ GC0 = new(),
+ GC1 = update(increment, 1, GC0),
+ GC2 = update(increment, 2, GC1),
+ GC3 = update(increment, 1, GC2),
+ ?assertEqual([{1, 2}, {2, 1}], GC3).
+
+update_increment_by_test() ->
+ GC0 = new(),
+ GC = update({increment, 7}, 1, GC0),
+ ?assertEqual([{1, 7}], GC).
+
+merge_test() ->
+ GC1 = [{<<"1">>, 1},
+ {<<"2">>, 2},
+ {<<"4">>, 4}],
+ GC2 = [{<<"3">>, 3},
+ {<<"4">>, 3}],
+ ?assertEqual([], merge(new(), new())),
+ ?assertEqual([{<<"1">>,1},{<<"2">>,2},{<<"3">>,3},{<<"4">>,4}],
+ lists:sort( merge(GC1, GC2))).
+
+merge_less_left_test() ->
+ GC1 = [{<<"5">>, 5}],
+ GC2 = [{<<"6">>, 6}, {<<"7">>, 7}],
+ ?assertEqual([{<<"5">>, 5},{<<"6">>,6}, {<<"7">>, 7}],
+ merge(GC1, GC2)).
+
+merge_less_right_test() ->
+ GC1 = [{<<"6">>, 6}, {<<"7">>,7}],
+ GC2 = [{<<"5">>, 5}],
+ ?assertEqual([{<<"5">>,5},{<<"6">>,6}, {<<"7">>, 7}],
+ lists:sort( merge(GC1, GC2)) ).
+
+merge_same_id_test() ->
+ GC1 = [{<<"1">>, 2},{<<"2">>,4}],
+ GC2 = [{<<"1">>, 3},{<<"3">>,5}],
+ ?assertEqual([{<<"1">>, 3},{<<"2">>,4},{<<"3">>,5}],
+ lists:sort( merge(GC1, GC2)) ).
+
+equal_test() ->
+ GC1 = [{1, 2}, {2, 1}, {4, 1}],
+ GC2 = [{1, 1}, {2, 4}, {3, 1}],
+ GC3 = [{1, 2}, {2, 1}, {4, 1}],
+ GC4 = [{4, 1}, {1, 2}, {2, 1}],
+ ?assertNot(equal(GC1, GC2)),
+ ?assert(equal(GC1, GC3)),
+ ?assert(equal(GC1, GC4)).
+
+usage_test() ->
+ GC1 = new(),
+ GC2 = new(),
+ ?assert(equal(GC1, GC2)),
+ GC1_1 = update({increment, 2}, a1, GC1),
+ GC2_1 = update(increment, a2, GC2),
+ GC3 = merge(GC1_1, GC2_1),
+ GC2_2 = update({increment, 3}, a3, GC2_1),
+ GC3_1 = update(increment, a4, GC3),
+ GC3_2 = update(increment, a1, GC3_1),
+ ?assertEqual([{a1, 3}, {a2, 1}, {a3, 3}, {a4, 1}],
+ lists:sort(merge(GC3_2, GC2_2))).
+
+
+-endif.
182 src/riak_kv_pncounter.erl
View
@@ -0,0 +1,182 @@
+%% -------------------------------------------------------------------
+%%
+%% riak_kv_pncounter: A convergent, replicated, state based PN counter
+%%
+%% Copyright (c) 2007-2012 Basho Technologies, Inc. All Rights Reserved.
+%%
+%% This file is provided to you under the Apache License,
+%% Version 2.0 (the "License"); you may not use this file
+%% except in compliance with the License. You may obtain
+%% a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+%%
+%% -------------------------------------------------------------------
+
+-module(riak_kv_pncounter).
+
+-ifdef(EQC).
+-include_lib("eqc/include/eqc.hrl").
+-endif.
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% API
+-export([new/0, new/2, value/1, update/3, merge/2, equal/2]).
+
+%% EQC API
+-ifdef(EQC).
+-export([gen_op/0, update_expected/3, eqc_state_value/1]).
+-endif.
+
+%% EQC generator
+-ifdef(EQC).
+gen_op() ->
+ oneof([increment, {increment, gen_pos()}, decrement, {decrement, gen_pos()} ]).
+
+gen_pos()->
+ ?LET(X, int(), 1+abs(X)).
+
+update_expected(_ID, increment, Prev) ->
+ Prev+1;
+update_expected(_ID, decrement, Prev) ->
+ Prev-1;
+update_expected(_ID, {increment, By}, Prev) ->
+ Prev+By;
+update_expected(_ID, {decrement, By}, Prev) ->
+ Prev-By;
+update_expected(_ID, _Op, Prev) ->
+ Prev.
+
+eqc_state_value(S) ->
+ S.
+-endif.
+
+new() ->
+ {riak_kv_gcounter:new(), riak_kv_gcounter:new()}.
+
+%% create a PN-Counter with an initial Op
+new(Actor, Value) when Value > 0 ->
+ update({increment, Value}, Actor, new());
+new(Actor, Value) when Value < 0 ->
+ update({decrement, Value * -1}, Actor, new());
+new(_Actor, _Zero) ->
+ new().
+
+value({Incr, Decr}) ->
+ riak_kv_gcounter:value(Incr) - riak_kv_gcounter:value(Decr).
+
+update(increment, Actor, {Incr, Decr}) ->
+ {riak_kv_gcounter:update(increment, Actor, Incr), Decr};
+update({increment, By}, Actor, {Incr, Decr}) when is_integer(By), By > 0 ->
+ {riak_kv_gcounter:update({increment, By}, Actor, Incr), Decr};
+update(decrement, Actor, {Incr, Decr}) ->
+ {Incr, riak_kv_gcounter:update(increment, Actor, Decr)};
+update({decrement, By}, Actor, {Incr, Decr}) when is_integer(By), By > 0 ->
+ {Incr, riak_kv_gcounter:update({increment, By}, Actor, Decr)}.
+
+merge({Incr1, Decr1}, {Incr2, Decr2}) ->
+ MergedIncr = riak_kv_gcounter:merge(Incr1, Incr2),
+ MergedDecr = riak_kv_gcounter:merge(Decr1, Decr2),
+ {MergedIncr, MergedDecr}.
+
+equal({Incr1, Decr1}, {Incr2, Decr2}) ->
+ riak_kv_gcounter:equal(Incr1, Incr2) andalso riak_kv_gcounter:equal(Decr1, Decr2).
+
+%% ===================================================================
+%% EUnit tests
+%% ===================================================================
+-ifdef(TEST).
+
+-ifdef(EQC).
+eqc_value_test_() ->
+ {timeout, 120, [?_assert(crdt_statem_eqc:prop_converge(0, 1000, ?MODULE))]}.
+-endif.
+
+new_test() ->
+ ?assertEqual({[], []}, new()).
+
+value_test() ->
+ PNCnt1 = {[{1, 1}, {2, 13}, {3, 1}], [{2, 10}, {4, 1}]},
+ PNCnt2 = {[], []},
+ PNCnt3 = {[{1, 3}, {2, 1}, {3, 1}], [{1, 3}, {2, 1}, {3, 1}]},
+ ?assertEqual(4, value(PNCnt1)),
+ ?assertEqual(0, value(PNCnt2)),
+ ?assertEqual(0, value(PNCnt3)).
+
+update_increment_test() ->
+ PNCnt0 = new(),
+ PNCnt1 = update(increment, 1, PNCnt0),
+ PNCnt2 = update(increment, 2, PNCnt1),
+ PNCnt3 = update(increment, 1, PNCnt2),
+ ?assertEqual({[{1, 2}, {2, 1}], []}, PNCnt3).
+
+update_increment_by_test() ->
+ PNCnt0 = new(),
+ PNCnt1 = update({increment, 7}, 1, PNCnt0),
+ ?assertEqual({[{1, 7}], []}, PNCnt1).
+
+update_decrement_test() ->
+ PNCnt0 = new(),
+ PNCnt1 = update(increment, 1, PNCnt0),
+ PNCnt2 = update(increment, 2, PNCnt1),
+ PNCnt3 = update(increment, 1, PNCnt2),
+ PNCnt4 = update(decrement, 1, PNCnt3),
+ ?assertEqual({[{1, 2}, {2, 1}], [{1, 1}]}, PNCnt4).
+
+update_decrement_by_test() ->
+ PNCnt0 = new(),
+ PNCnt1 = update({increment, 7}, 1, PNCnt0),
+ PNCnt2 = update({decrement, 5}, 1, PNCnt1),
+ ?assertEqual({[{1, 7}], [{1, 5}]}, PNCnt2).
+
+merge_test() ->
+ PNCnt1 = {[{<<"1">>, 1},
+ {<<"2">>, 2},
+ {<<"4">>, 4}], []},
+ PNCnt2 = {[{<<"3">>, 3},
+ {<<"4">>, 3}], []},
+ ?assertEqual({[], []}, merge(new(), new())),
+ ?assertEqual({[{<<"1">>,1},{<<"2">>,2},{<<"4">>,4},{<<"3">>,3}], []},
+ merge(PNCnt1, PNCnt2)).
+
+merge_too_test() ->
+ PNCnt1 = {[{<<"5">>, 5}], [{<<"7">>, 4}]},
+ PNCnt2 = {[{<<"6">>, 6}, {<<"7">>, 7}], [{<<"5">>, 2}]},
+ ?assertEqual({[{<<"5">>, 5},{<<"6">>,6}, {<<"7">>, 7}], [{<<"7">>, 4}, {<<"5">>, 2}]},
+ merge(PNCnt1, PNCnt2)).
+
+equal_test() ->
+ PNCnt1 = {[{1, 2}, {2, 1}, {4, 1}], [{1, 1}, {3, 1}]},
+ PNCnt2 = {[{1, 1}, {2, 4}, {3, 1}], []},
+ PNCnt3 = {[{1, 2}, {2, 1}, {4, 1}], [{3, 1}, {1, 1}]},
+ PNCnt4 = {[{4, 1}, {1, 2}, {2, 1}], [{1, 1}, {3, 1}]},
+ ?assertNot(equal(PNCnt1, PNCnt2)),
+ ?assert(equal(PNCnt3, PNCnt4)),
+ ?assert(equal(PNCnt1, PNCnt3)).
+
+usage_test() ->
+ PNCnt1 = new(),
+ PNCnt2 = new(),
+ ?assert(equal(PNCnt1, PNCnt2)),
+ PNCnt1_1 = update({increment, 2}, a1, PNCnt1),
+ PNCnt2_1 = update(increment, a2, PNCnt2),
+ PNCnt3 = merge(PNCnt1_1, PNCnt2_1),
+ PNCnt2_2 = update({increment, 3}, a3, PNCnt2_1),
+ PNCnt3_1 = update(increment, a4, PNCnt3),
+ PNCnt3_2 = update(increment, a1, PNCnt3_1),
+ PNCnt3_3 = update({decrement, 2}, a5, PNCnt3_2),
+ PNCnt2_3 = update(decrement, a2, PNCnt2_2),
+ ?assertEqual({[{a1, 3}, {a4, 1}, {a2, 1}, {a3, 3}], [{a5, 2}, {a2, 1}]},
+ merge(PNCnt3_3, PNCnt2_3)).
+
+-endif.
3  src/riak_kv_put_fsm.erl
View
@@ -603,6 +603,9 @@ handle_options([{returnbody, false}|T], State = #state{postcommit = Postcommit})
dw=erlang:max(1,State#state.dw),
returnbody=false})
end;
+handle_options([{counter_op, _Amt}=COP|T], State) ->
+ VNodeOpts = [COP | State#state.vnode_options],
+ handle_options(T, State#state{vnode_options=VNodeOpts});
handle_options([{_,_}|T], State) -> handle_options(T, State).
init_putcore(State = #state{n = N, w = W, dw = DW, allowmult = AllowMult,
66 src/riak_kv_vnode.erl
View
@@ -108,7 +108,8 @@
bprops :: maybe_improper_list(),
starttime :: non_neg_integer(),
prunetime :: undefined| non_neg_integer(),
- is_index=false :: boolean() %% set if the b/end supports indexes
+ is_index=false :: boolean(), %% set if the b/end supports indexes
+ counter_op = undefined :: undefined | integer() %% if set this is a counter operation
}).
-spec maybe_create_hashtrees(state()) -> state().
@@ -753,6 +754,7 @@ do_put(Sender, {Bucket,_Key}=BKey, RObj, ReqID, StartTime, Options, State) ->
PruneTime = StartTime
end,
Coord = proplists:get_value(coord, Options, false),
+ CounterOp = proplists:get_value(counter_op, Options, undefined),
PutArgs = #putargs{returnbody=proplists:get_value(returnbody,Options,false) orelse Coord,
coord=Coord,
lww=proplists:get_value(last_write_wins, BProps, false),
@@ -761,7 +763,8 @@ do_put(Sender, {Bucket,_Key}=BKey, RObj, ReqID, StartTime, Options, State) ->
reqid=ReqID,
bprops=BProps,
starttime=StartTime,
- prunetime=PruneTime},
+ prunetime=PruneTime,
+ counter_op = CounterOp},
{PrepPutRes, UpdPutArgs} = prepare_put(State, PutArgs),
{Reply, UpdState} = perform_put(PrepPutRes, State, UpdPutArgs),
riak_core_vnode:reply(Sender, Reply),
@@ -827,7 +830,8 @@ prepare_put(#state{vnodeid=VId,
coord=Coord,
lww=LWW,
starttime=StartTime,
- prunetime=PruneTime},
+ prunetime=PruneTime,
+ counter_op = CounterOp},
IndexBackend) ->
case Mod:get(Bucket, Key, ModState) of
{error, not_found, _UpdModState} ->
@@ -838,8 +842,18 @@ prepare_put(#state{vnodeid=VId,
IndexSpecs = []
end,
ObjToStore = case Coord of
- true ->
- riak_object:increment_vclock(RObj, VId, StartTime);
+ true ->
+ VClockUp = riak_object:increment_vclock(RObj, VId, StartTime),
+ case CounterOp of
+ %% coordinating a counter operation means
+ %% creating / incrementing the counter
+ Val when is_integer(Val) ->
+ %% make a new counter, stuff it in the riak_object
+ Counter = riak_kv_pncounter:new(VId, CounterOp),
+ Incremented = riak_object:update_value(VClockUp, {riak_kv_pncounter, Counter}),
+ riak_object:apply_updates(Incremented);
+ _ -> VClockUp
+ end;
false ->
RObj
end,
@@ -852,29 +866,37 @@ prepare_put(#state{vnodeid=VId,
{newobj, NewObj} ->
VC = riak_object:vclock(NewObj),
AMObj = enforce_allow_mult(NewObj, BProps),
- case IndexBackend of
- true ->
- IndexSpecs =
- riak_object:diff_index_specs(AMObj,
+ IndexSpecs = case IndexBackend of
+ true ->
+ riak_object:diff_index_specs(AMObj,
OldObj);
- false ->
- IndexSpecs = []
+ false ->
+ []
end,
- case PruneTime of
- undefined ->
- ObjToStore = AMObj;
- _ ->
- ObjToStore =
- riak_object:set_vclock(AMObj,
- vclock:prune(VC,
- PruneTime,
- BProps))
+ ObjToStore = case PruneTime of
+ undefined ->
+ AMObj;
+ _ ->
+ riak_object:set_vclock(AMObj,
+ vclock:prune(VC,
+ PruneTime,
+ BProps))
end,
- {{true, ObjToStore},
+ ObjToStore2 = handle_counter(Coord, CounterOp, VId, ObjToStore, IndexSpecs),
+ {{true, ObjToStore2},
PutArgs#putargs{index_specs=IndexSpecs, is_index=IndexBackend}}
end
end.
+handle_counter(true, CounterOp, VId, RObj, IndexSpecs) when is_integer(CounterOp) ->
+ riak_kv_counter:update(RObj, IndexSpecs, VId, CounterOp);
+handle_counter(false, CounterOp, _Vid, RObj, IndexSpecs) when is_integer(CounterOp) ->
+ %% non co-ord put, merge the values if there are siblings
+ %% 'cos that is the point of CRDTs / counters: no siblings
+ riak_kv_counter:merge(RObj, IndexSpecs);
+handle_counter(_Coord, __CounterOp, _VId, RObj, _IndexSpecs) ->
+ RObj.
+
perform_put({false, Obj},
#state{idx=Idx}=State,
#putargs{returnbody=true,
@@ -945,7 +967,7 @@ put_merge(false, false, CurObj, UpdObj, _VId, _StartTime) -> % coord=false, LWW=
false ->
{newobj, ResObj}
end;
-put_merge(true, true, _CurObj, UpdObj, VId, StartTime) -> % coord=false, LWW=true
+put_merge(true, true, _CurObj, UpdObj, VId, StartTime) -> % coord=true, LWW=true
{newobj, riak_object:increment_vclock(UpdObj, VId, StartTime)};
put_merge(true, false, CurObj, UpdObj, VId, StartTime) ->
UpdObj1 = riak_object:increment_vclock(UpdObj, VId, StartTime),
123 test/crdt_statem_eqc.erl
View
@@ -0,0 +1,123 @@
+%% -------------------------------------------------------------------
+%%
+%% crdt_statem_eqc: Quickcheck statem test for riak_dt modules
+%%
+%% Copyright (c) 2007-2012 Basho Technologies, Inc. All Rights Reserved.
+%%
+%% This file is provided to you under the Apache License,
+%% Version 2.0 (the "License"); you may not use this file
+%% except in compliance with the License. You may obtain
+%% a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+%%
+%% -------------------------------------------------------------------
+
+-module(crdt_statem_eqc).
+
+-ifdef(EQC).
+-include_lib("eqc/include/eqc.hrl").
+-include_lib("eqc/include/eqc_statem.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-compile(export_all).
+
+-record(state,{vnodes=[], mod_state, vnode_id=0, mod}).
+
+-define(NUMTESTS, 1000).
+-define(QC_OUT(P),
+ eqc:on_output(fun(Str, Args) ->
+ io:format(user, Str, Args) end, P)).
+
+%% Initialize the state
+initial_state() ->
+ #state{}.
+
+%% Command generator, S is the state
+command(#state{vnodes=VNodes, mod=Mod}) ->
+ oneof([{call, ?MODULE, create, [Mod]}] ++
+ [{call, ?MODULE, update, [Mod, Mod:gen_op(), elements(VNodes)]} || length(VNodes) > 0] ++ %% If a vnode exists
+ [{call, ?MODULE, merge, [Mod, elements(VNodes), elements(VNodes)]} || length(VNodes) > 0] ++
+ [{call, ?MODULE, crdt_equals, [Mod, elements(VNodes), elements(VNodes)]} || length(VNodes) > 0]
+).
+
+%% Next state transformation, S is the current state
+next_state(#state{vnodes=VNodes, mod=Mod, vnode_id=ID, mod_state=Expected0}=S,V,{call,?MODULE,create,_}) ->
+ Expected = Mod:update_expected(ID, create, Expected0),
+ S#state{vnodes=VNodes++[{ID, V}], vnode_id=ID+1, mod_state=Expected};
+next_state(#state{vnodes=VNodes0, mod_state=Expected, mod=Mod}=S,V,{call,?MODULE, update, [Mod, Op, {ID, _C}]}) ->
+ VNodes = lists:keyreplace(ID, 1, VNodes0, {ID, V}),
+ S#state{vnodes=VNodes, mod_state=Mod:update_expected(ID, Op, Expected)};
+next_state(#state{vnodes=VNodes0, mod_state=Expected0, mod=Mod}=S,V,{call,?MODULE, merge, [_Mod, {IDS, _C}=_Source, {ID, _C}=_Dest]}) ->
+ VNodes = lists:keyreplace(ID, 1, VNodes0, {ID, V}),
+ Expected = Mod:update_expected(ID, {merge, IDS}, Expected0),
+ S#state{vnodes=VNodes, mod_state=Expected};
+next_state(S, _V, _C) ->
+ S.
+
+%% Precondition, checked before command is added to the command sequence
+precondition(_S,{call,_,_,_}) ->
+ true.
+
+%% Postcondition, checked after command has been evaluated
+%% OBS: S is the state before next_state(S,_,<command>)
+postcondition(_S,{call,?MODULE, crdt_equals, _},Res) ->
+ Res == true;
+postcondition(_S,{call,_,_,_},_Res) ->
+ true.
+
+prop_converge(InitialValue, NumTests, Mod) ->
+ eqc:quickcheck(eqc:numtests(NumTests, ?QC_OUT(prop_converge(InitialValue, Mod)))).
+
+prop_converge(InitialValue, Mod) ->
+ ?FORALL(Cmds,commands(?MODULE, #state{mod=Mod, mod_state=InitialValue}),
+ begin
+ {H,S,Res} = run_commands(?MODULE,Cmds),
+ Merged = merge_crdts(Mod, S#state.vnodes),
+ MergedVal = Mod:value(Merged),
+ ExpectedValue = Mod:eqc_state_value(S#state.mod_state),
+ ?WHENFAIL(
+ %% History: ~p\nState: ~p\ H,S,
+ io:format("History: ~p\nState: ~p", [H,S]),
+ conjunction([{res, equals(Res, ok)},
+ {total, equals(sort(MergedVal), sort(ExpectedValue))}]))
+ end).
+
+merge_crdts(Mod, []) ->
+ Mod:new();
+merge_crdts(Mod, [{_ID, Crdt}|Crdts]) ->
+ lists:foldl(fun({_ID0, C}, Acc) ->
+ Mod:merge(C, Acc) end,
+ Crdt,
+ Crdts).
+
+%% Commands
+create(Mod) ->
+ Mod:new().
+
+update(Mod, Op, {ID, C}) ->
+ Mod:update(Op, ID, C).
+
+merge(Mod, {_IDS, CS}, {_IDD, CD}) ->
+ Mod:merge(CS, CD).
+
+crdt_equals(Mod, {_IDS, CS}, {_IDD, CD}) ->
+ Mod:equal(Mod:merge(CS, CD),
+ Mod:merge(CD, CS)).
+
+%% Helpers
+%% The orset CRDT returns a list, it has no guarantees about order
+%% list equality expects lists in order
+sort(L) when is_list(L) ->
+ lists:sort(L);
+sort(Other) ->
+ Other.
+
+-endif. % EQC
Please sign in to comment.
Something went wrong with that request. Please try again.