From 21cd03477af56cab3cddcb363d6a62aff9c56bcd Mon Sep 17 00:00:00 2001 From: Russell Brown Date: Mon, 8 Dec 2014 14:38:22 +0000 Subject: [PATCH 01/33] WIP smaller map, maybe --- src/riak_dt_map2.erl | 807 +++++++++++++++++++++++++++++++++++++++++++ test/map2_eqc.erl | 685 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1492 insertions(+) create mode 100644 src/riak_dt_map2.erl create mode 100644 test/map2_eqc.erl diff --git a/src/riak_dt_map2.erl b/src/riak_dt_map2.erl new file mode 100644 index 0000000..837c636 --- /dev/null +++ b/src/riak_dt_map2.erl @@ -0,0 +1,807 @@ +%% ------------------------------------------------------------------- +%% +%% riak_dt_map2: OR-Set schema based multi CRDT container +%% +%% Copyright (c) 2007-2013 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 multi CRDT holder. A Struct/Document-ish thing. Uses the +%% same tombstone-less, Observed Remove semantics as `riak_dt_orswot'. +%% A Map is set of `Field's a `Field' is a two-tuple of: +%% `{Name::binary(), CRDTModule::module()}' where the second element +%% is the name of a crdt module that may be embedded. CRDTs stored +%% inside the Map will have their `update/3/4' function called, but the +%% second argument will be a `riak_dt:dot()', so that they share the +%% causal context of the map, even when fields are removed, and +%% subsequently re-added. +%% +%% The contents of the Map are modeled as a dictionary of +%% `field_name()' to `field_value()' mappings. Where `field_ name()' +%% is a two tuple of an opaque `binary()' name, and one of the +%% embeddable crdt types (currently `riak_dt_orswot', +%% `riak_dt_emcntr', `riak_dt_lwwreg', `riak_dt_od_flag', and +%% `riak_dt_map'). The reason for this limitation is that embedded +%% types must support embedding: that is a shared, `dot'-based, causal +%% context, and a reset-remove semantic (more on these below.) The +%% `field_value()' is a two-tuple of `entries()' and a +%% `tombstone()'. The presence of a `tombstone()' in a "tombstoneless" +%% Map is confusing. The `tombstone()' is only stored for fields that +%% are currently in the map, removing a field also removes its +%% tombstone. +%% +%% To use the Map create a `new()' Map. When you call `update/3' or +%% `update/4' you pass a list of operations and an optional causal +%% context. @See `update/3' or `update/4' for more details. The list +%% of operations is applied atomically in full, and new state +%% returned, or not at all, and an error is returned. +%% +%%

Semantics

+%% +%% The semantics of this Map are Observed-Remove-Reset-Remove. What +%% this means is practice is, if a field is removed, and concurrently +%% that same field is updated, the field is _in_ the Map (only +%% observed updates are removed) but those removes propagate, so only +%% the concurrent update survives. A concrete example helps: If a Map +%% contains a field that is a set, and the set has 5 elements in it, +%% and concurrently the replica at A removes the field that contains +%% the set, while the replica at B adds an item to the set, on merge +%% there is a field for the set, but it contains only the one item B +%% added. The removal of the field is semantically equivalent to +%% removing all elements in the set, and removing the field. The same +%% goes for an embedded Map. If concurrently a Map field is removed, +%% while a new sub-field is updated, only the updated field(s) survive +%% the reset-remove. +%% +%% There is an anomaly for embedded counters that does not fully +%% support reset remove. Embedded counters (@see riak_dt_emcntr) are +%% different to a normal `pn-counter'. Embedded counters map `dot's to +%% {P, N} pairs. When a counter is incremented a new dot is created, +%% that replaces the old dot with the new value. `pn-counter' usually +%% merges by taking the `max' of any `P' or `N' entry for an +%% actor. This does not work in an embedded context. When a counter +%% field is removed, and then _re_-added, the new `P' and `N' entries +%% may be lower than the old, and merging loses the remove +%% information. However, if a `dot' is stored with the value, and the +%% max of the `dot' is used in merge, new updates win over removed +%% updates. So far so good. Here is the problem. If Replica B removes +%% a counter field, and does not re-add it, and replica A concurrently +%% updates it's entry for that field, then the reset-remove does not +%% occur. All new dots are not `observed' by Replica B, so not +%% removed. The new `dots' contain the updates from the previous +%% `dots', and the old `dot' is discarded. To achieve reset-remove all +%% increments would need a dot, and need to be retained, which would +%% be very costly in terms of space. One way to accept this anomaly is +%% to think of a Map like a file system: removing a directory and +%% concurrently adding a file means that the directory is present and +%% only the file remains in it. Updating a counter and concurrently +%% removing it, means the counter remains, with the updated value, +%% much like appending to a file in the file system analogy: you don't +%% expect only the diff to survive, but the whole updated file. +%% +%%

Merging/Size

+%% +%% When any pair of Maps are merged, the embedded CRDTs are _not_ +%% merged, instead each concurrent `dot'->`field()' entry is +%% kept. This leads to a greater size for Maps that are highly +%% divergent. Updating a field in the map, however, leads to all +%% entries for that field being merged to a single CRDT that is stored +%% against the new `dot'. As mentioned above, there is also a +%% `tombstone' entry per present field. This is bottom CRDT for the +%% field type with a clock that contains all seen and removed +%% `dots'. There tombstones are merged at merge time, so only one is +%% present per field. Clearly the repetition of actor information (the +%% clock, each embedded CRDT, the field `dots', the tombstones) is a +%% serious issue with regard to size/bloat of this data type. We use +%% erlang's `to_binary/2' function, which compresses the data, to get +%% around this at present. +%% +%%

Context and Deferred operations

+%% +%% For CRDTs that use version vectors and dots (this `Map' and all +%% CRDTs that may be embedded in it), the size of the CRDT is +%% influenced by the number of actors updating it. In some systems +%% (like Riak!) we attempt to minimize the number of actors by only +%% having the database update CRDTs. This leads to a kind of "action +%% at a distance", where a client sends operations to the database, +%% and an actor in the database system performs the operations. The +%% purpose is to ship minimal state between database and client, and +%% to limit the number of actors in the system. There is a problem +%% with action at a distance and the OR semantic. The client _must_ be +%% able to tell the database what has been observed when it sends a +%% remove operation. There is a further problem. A replica that +%% handles an operation may not have all the state the client +%% observed. We solve these two problems by asking the client to +%% provide a causal context for operations (@see `update/4'.) Context +%% operations solve the OR problem, but they don't solve the problem +%% of lagging replicas handling operations. +%% +%%

Lagging replicas, deferred operations

+%% +%% In a system like Riak, a replica that is not up-to-date (including, +%% never seen any state for a CRDT) maybe asked to perform an +%% operation. If no context is given, and the operation is a field +%% remove, or a "remove" like operation on an embedded CRDT, the +%% operation may fail with a precondition error (for example, remove a +%% field that is not present) or succeed and remove more state than +%% intended (a field remove with no context may remove updates unseen +%% by the client.) When a context is provided, and the Field to be +%% removed is absent, the Map state stores the context, and Field +%% name, in a list of deferred operations. When, eventually, through +%% propagation and merging, the Map's clock descends the context for +%% the operation, the operation is executed. It is important to note +%% that _only_ actorless (field remove) operations can occur this way. +%% +%%

Embedded CRDTs Deferred Operations

+%% +%% There is a bug with embedded types and deferred operations. Imagine +%% a client has seen a Map with a Set field, and the set contains {a, +%% b, c}. The client sends an operation to remove {a} from the set. A +%% replica that is new takes the operation. It will create a new Map, +%% a Field for the Set, and store the `remove` operation as part of +%% the Set's state. A client reads this new state, and sends a field +%% remove operation, that is executed by same replica. Now the +%% deferred operation is lost, since the field is removed. We're +%% working on ways to fix this. One idea is to not remove a field with +%% "undelivered" operations, but instead to "hide" it. +%% +%% @see riak_dt_orswot for more on the OR semantic +%% @see riak_dt_emcntr for the embedded counter. +%% @end + +-module(riak_dt_map2). + +-behaviour(riak_dt). + +-ifdef(EQC). +-include_lib("eqc/include/eqc.hrl"). +-endif. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +%% API +-export([new/0, value/1, value/2, update/3, update/4]). +-export([merge/2, equal/2, to_binary/1, from_binary/1]). +-export([precondition_context/1, stats/1, stat/2]). +-export([parent_clock/2]). + +%% EQC API +-ifdef(EQC). +-export([gen_op/0, gen_op/1, gen_field/0, gen_field/1, generate/0, size/1]). +-endif. + +-export_type([map/0, binary_map/0, map_op/0]). + +-type binary_map() :: binary(). %% A binary that from_binary/1 will accept +-type map() :: {riak_dt_vclock:vclock(), entries(), deferred()}. +-type entries() :: [field()]. +-type field() :: {field_name(), field_value()}. +-type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. +-type field_value() :: {riak_dt_vclock:vclock(), crdt()}. + +%%-type crdts() :: [entry()]. +%%-type entry() :: {riak_dt:dot(), crdt()}. + +%% Only for present fields, ensures removes propogate +%%-type tombstone() :: crdt(). + +%% Only field removals can be deferred. CRDTs stored in the map may +%% have contexts and deferred operations, but as these are part of the +%% state, they are stored under the field as an update like any other. +-type deferred() :: [{context(), [field()]}]. + +%% limited to only those mods that support both a shared causal +%% context, and by extension, the reset-remove semantic. +-type crdt_mod() :: riak_dt_emcntr | riak_dt_lwwreg | + riak_dt_od_flag | + riak_dt_map | riak_dt_orswot. + +-type crdt() :: riak_dt_emcntr:emcntr() | riak_dt_od_flag:od_flag() | + riak_dt_lwwreg:lwwreg() | + riak_dt_orswot:orswot() | + riak_dt_map:map(). + +-type map_op() :: {update, [map_field_update() | map_field_op()]}. + +-type map_field_op() :: {remove, field()}. +-type map_field_update() :: {update, field(), crdt_op()}. + +-type crdt_op() :: riak_dt_emcntr:emcntr_op() | + riak_dt_lwwreg:lwwreg_op() | + riak_dt_orswot:orswot_op() | riak_dt_od_flag:od_flag_op() | + riak_dt_map:map_op(). + +-type context() :: riak_dt_vclock:vclock() | undefined. + +-type values() :: [value()]. +-type value() :: {field(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. +-type precondition_error() :: {error, {precondition, {not_present, field()}}}. + +-define(FRESH_CLOCK, riak_dt_vclock:fresh()). + +%% @doc Create a new, empty Map. +-spec new() -> map(). +new() -> + {riak_dt_vclock:fresh(), orddict:new(), orddict:new()}. + +%% @doc sets the clock in the map to that `Clock'. Used by a +%% containing Map for sub-CRDTs +-spec parent_clock(riak_dt_vclock:vclock(), map()) -> map(). +parent_clock(Clock, {_MapClock, Values, Deferred}) -> + {Clock, Values, Deferred}. + +%% @doc get the current set of values for this Map +-spec value(map()) -> values(). +value({_Clock, Values, _Deferred}) -> + orddict:fold(fun({Name, Type}, {_Dots, CRDT}, Acc) -> + [{{Name, Type}, Type:value(CRDT)} | Acc] end, + [], + Values). + +%% @doc query map (not implemented yet) +-spec value(term(), map()) -> values(). +value(_, Map) -> + value(Map). + +%% @doc update the `map()' or a field in the `map()' by executing +%% the `map_op()'. `Ops' is a list of one or more of the following +%% ops: +%% +%% `{update, field(), Op} where `Op' is a valid update operation for a +%% CRDT of type `Mod' from the `Key' pair `{Name, Mod}' If there is no +%% local value for `Key' a new CRDT is created, the operation applied +%% and the result inserted otherwise, the operation is applied to the +%% local value. +%% +%% `{remove, `field()'}' where field is `{name, type}', results in +%% the crdt at `field' and the key and value being removed. A +%% concurrent `update' will "win" over a remove so that the field is +%% still present, and it's value will contain the concurrent update. +%% +%% Atomic, all of `Ops' are performed successfully, or none are. +-spec update(map_op(), riak_dt:actor() | riak_dt:dot(), map()) -> + {ok, map()} | precondition_error(). +update(Op, ActorOrDot, Map) -> + update(Op, ActorOrDot, Map, undefined). + +%% @doc the same as `update/3' except that the context ensures no +%% unseen field updates are removed, and removal of unseen updates is +%% deferred. The Context is passed down as the context for any nested +%% types. hence the common clock. +%% +%% @see parent_clock/2 +-spec update(map_op(), riak_dt:actor() | riak_dt:dot(), map(), riak_dt:context()) -> + {ok, map()}. +update({update, Ops}, ActorOrDot, {Clock0, Values, Deferred}, Ctx) -> + {Dot, Clock} = update_clock(ActorOrDot, Clock0), + apply_ops(Ops, Dot, {Clock, Values, Deferred}, Ctx). + +%% @private update the clock, and get a dot for the operations. This +%% means that field removals increment the clock too. +-spec update_clock(riak_dt:actor() | riak_dt:dot(), + riak_dt_vclock:vclock()) -> + {riak_dt:dot(), riak_dt_vclock:vclock()}. +update_clock(Dot, Clock) when is_tuple(Dot) -> + NewClock = riak_dt_vclock:merge([[Dot], Clock]), + {Dot, NewClock}; +update_clock(Actor, Clock) -> + NewClock = riak_dt_vclock:increment(Actor, Clock), + Dot = {Actor, riak_dt_vclock:get_counter(Actor, NewClock)}, + {Dot, NewClock}. + +get({_Name, Type}=Field, Fields, Clock) -> + CRDT = case orddict:find(Field, Fields) of + {ok, {_Dots, CRDT0}} -> + CRDT0; + error -> + Type:new() + end, + Type:parent_clock(Clock, CRDT). + +get_entry({_Name, Type}=Field, Fields, Clock) -> + {Dots, CRDT} = case orddict:find(Field, Fields) of + {ok, Entry} -> + Entry; + error -> + {[], Type:new()} + end, + {Dots, Type:parent_clock(Clock, CRDT)}. + +%% @private +-spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), + {riak_dt_vclock:vclock(), entries() , deferred()}, context()) -> + {ok, map()} | precondition_error(). +apply_ops([], _Dot, Map, _Ctx) -> + {ok, Map}; +apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> + CRDT = get(Field, Values, Clock), + case Type:update(Op, Dot, CRDT, Ctx) of + {ok, Updated0} -> + Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), + NewValues = orddict:store(Field, {[Dot], Updated}, Values), + apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); + Error -> + Error + end; +apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> + case remove_field(Field, Map, Ctx) of + {ok, NewMap} -> + apply_ops(Rest, Dot, NewMap, Ctx); + E -> + E + end. + +%% @private when context is undefined, we simply remove all instances +%% of Field, regardless of their dot. If the field is not present then +%% we warn the user with a precondition error. However, in the case +%% that a context is provided we can be more fine grained, and only +%% remove those field entries whose dots are seen by the context. This +%% preserves the "observed" part of "observed-remove". There is no +%% precondition error if we're asked to remove smoething that isn't +%% present, either we defer it, or it has been done already, depending +%% on if the Map clock descends the context clock or not. +%% +%% @see defer_remove/4 for handling of removes of fields that are +%% _not_ present +-spec remove_field(field(), map(), context()) -> + {ok, map()} | precondition_error(). +remove_field(Field, {Clock, Values, Deferred}, undefined) -> + case orddict:find(Field, Values) of + error -> + {error, {precondition, {not_present, Field}}}; + {ok, _Removed} -> + {ok, {Clock, orddict:erase(Field, Values), Deferred}} + end; +%% Context removes +remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> + Deferred = defer_remove(Clock, Ctx, Field, Deferred0), + NewValues = case ctx_rem_field(Field, Values, Ctx, Clock) of + empty -> + orddict:erase(Field, Values); + CRDT -> + orddict:store(Field, CRDT, Values) + end, + {ok, {Clock, NewValues, Deferred}}. + +%% @private drop dominated fields +ctx_rem_field(_Field, error, _Ctx_, _Clock) -> + empty; +ctx_rem_field({_, Type}, {ok, {Dots, CRDT}}, Ctx, MapClock) -> + %% Drop dominated fields, and update the tombstone. + %% + %% If the context is removing a field at dot {a, 1} and the + %% current field is {a, 2}, the tombstone ensures that all events + %% from {a, 1} are removed from the crdt value. If the ctx remove + %% is at {a, 3} and the current field is at {a, 2} then we need to + %% remove only events upto {a, 2}. The glb clock enables that. + %% + TombstoneClock = riak_dt_vclock:glb(Ctx, MapClock), %% GLB is events seen by both clocks only + TS = Type:parent_clock(TombstoneClock, Type:new()), + SurvivingDots = riak_dt_vclock:subtract_dots(Dots, Ctx), + case SurvivingDots of + [] -> %% Ctx remove removed all dots for field + empty; + _ -> + %% Update the tombstone with the GLB clock + CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), + %% Always reset to empty clock so we don't duplicate storage + {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2)} + end; +ctx_rem_field(Field, Values, Ctx, MapClock) -> + ctx_rem_field(Field, orddict:find(Field, Values), Ctx, MapClock). + +%% @private If we're asked to remove something we don't have (or have, +%% but maybe not all 'updates' for it), is it because we've not seen +%% the some update that we've been asked to remove, or is it because +%% we already removed it? In the former case, we can "defer" this +%% operation by storing it, with its context, for later execution. If +%% the clock for the Map descends the operation clock, then we don't +%% need to defer the op, its already been done. It is _very_ important +%% to note, that only _actorless_ operations can be saved. That is +%% operations that DO NOT need to increment the clock. In a Map this +%% means field removals only. Contexts for update operations do not +%% result in deferred operations on the parent Map. This simulates +%% causal delivery, in that an `update' must be seen before it can be +%% `removed'. +-spec defer_remove(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), field(), deferred()) -> + deferred(). +defer_remove(Clock, Ctx, Field, Deferred) -> + case riak_dt_vclock:descends(Clock, Ctx) of + %% no need to save this remove, we're done + true -> Deferred; + false -> orddict:update(Ctx, + fun(Fields) -> + ordsets:add_element(Field, Fields) end, + ordsets:add_element(Field, ordsets:new()), + Deferred) + end. + +merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) -> + Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), + Fields = lists:umerge(orddict:fetch_keys(LHSEntries), orddict:fetch_keys(RHSEntries)), + Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> + {LHSDots, LHSCRDT} = get_entry(Field, LHSEntries, LHSClock), + {RHSDots, RHSCRDT} = get_entry(Field, RHSEntries, RHSClock), + case keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) of + [] -> + Acc; + Dots -> + CRDT = Type:merge(LHSCRDT, RHSCRDT), + %% Yes! Reset the clock, again + orddict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT)}, Acc) + end + end, + orddict:new(), + Fields), + Deferred = merge_deferred(LHSDeferred, RHSDeferred), + apply_deferred(Clock, Entries, Deferred). + +keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> + CommonDots = sets:intersection(sets:from_list(LHSDots), sets:from_list(RHSDots)), + LHSUnique = sets:to_list(sets:subtract(sets:from_list(LHSDots), CommonDots)), + RHSUnique = sets:to_list(sets:subtract(sets:from_list(RHSDots), CommonDots)), + LHSKeep = riak_dt_vclock:subtract_dots(LHSUnique, RHSClock), + RHSKeep = riak_dt_vclock:subtract_dots(RHSUnique, LHSClock), + riak_dt_vclock:merge([sets:to_list(CommonDots), LHSKeep, RHSKeep]). + + +%% @private +-spec merge_deferred(deferred(), deferred()) -> deferred(). +merge_deferred(LHS, RHS) -> + orddict:merge(fun(_K, LH, RH) -> + ordsets:union(LH, RH) end, + LHS, RHS). + +%% @private apply those deferred field removals, if they're +%% preconditions have been met, that is. +-spec apply_deferred(riak_dt_vclock:vclock(), entries(), deferred()) -> + {riak_dt_vclock:vclock(), entries(), deferred()}. +apply_deferred(Clock, Entries, Deferred) -> + lists:foldl(fun({Ctx, Fields}, Map) -> + remove_all(Fields, Map, Ctx) + end, + {Clock, Entries, []}, + Deferred). + +%% @private +-spec remove_all([field()], map(), context()) -> + map(). +remove_all(Fields, Map, Ctx) -> + lists:foldl(fun(Field, MapAcc) -> + {ok, MapAcc2}= remove_field(Field, MapAcc, Ctx), + MapAcc2 + end, + Map, + Fields). + +%% @doc compare two `map()'s for equality of structure Both schemas +%% and value list must be equal. Performs a pariwise equals for all +%% values in the value lists +-spec equal(map(), map()) -> boolean(). +equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> + riak_dt_vclock:equal(Clock1, Clock2) andalso + Deferred1 == Deferred2 andalso + pairwise_equals(Values1, Values2). + +-spec pairwise_equals(entries(), entries()) -> boolean(). +pairwise_equals([], []) -> + true; +pairwise_equals([{{Name, Type}, {Dots1, CRDT1}}|Rest1], [{{Name, Type}, {Dots2, CRDT2}}|Rest2]) -> + case {riak_dt_vclock:equal(Dots1, Dots2), Type:equal(CRDT1, CRDT2)} of + {true, true} -> + pairwise_equals(Rest1, Rest2); + _ -> + false + end; +pairwise_equals(_, _) -> + false. + +%% @doc an opaque context that can be passed to `update/4' to ensure +%% that only seen fields are removed. If a field removal operation has +%% a context that the Map has not seen, it will be deferred until +%% causally relevant. +-spec precondition_context(map()) -> riak_dt:context(). +precondition_context({Clock, _Field, _Deferred}) -> + Clock. + +%% @doc stats on internal state of Map. +%% A proplist of `{StatName :: atom(), Value :: integer()}'. Stats exposed are: +%% `actor_count': The number of actors in the clock for the Map. +%% `field_count': The total number of fields in the Map (including divergent field entries). +%% `duplication': The number of duplicate entries in the Map across all fields. +%% basically `field_count' - ( unique fields) +%% `deferred_length': How many operations on the deferred list, a reasonable expression +%% of lag/staleness. +-spec stats(map()) -> [{atom(), integer()}]. +stats(Map) -> + [ {S, stat(S, Map)} || S <- [actor_count, field_count, duplication, deferred_length]]. + +-spec stat(atom(), map()) -> number() | undefined. +stat(actor_count, {Clock, _, _}) -> + length(Clock); +stat(field_count, {_, Fields, _}) -> + length(Fields); +stat(duplication, {_, Fields, _}) -> + %% Number of duplicated fields + {FieldCnt, Duplicates} = orddict:fold(fun(_Field, {Dots ,_}, {FCnt, DCnt}) -> + {FCnt+1, DCnt + orddict:size(Dots)} + end, + {0, 0}, + Fields), + Duplicates - FieldCnt; +stat(deferred_length, {_, _, Deferred}) -> + length(Deferred); +stat(_,_) -> undefined. + +-include("riak_dt_tags.hrl"). +-define(TAG, ?DT_MAP_TAG). +-define(V1_VERS, 1). + +%% @doc returns a binary representation of the provided `map()'. The +%% resulting binary is tagged and versioned for ease of future +%% upgrade. Calling `from_binary/1' with the result of this function +%% will return the original map. Use the application env var +%% `binary_compression' to turn t2b compression on (`true') and off +%% (`false') +%% +%% @see `from_binary/1' +-spec to_binary(map()) -> binary_map(). +to_binary(Map) -> + <>. + +%% @doc When the argument is a `binary_map()' produced by +%% `to_binary/1' will return the original `map()'. +%% +%% @see `to_binary/1' +-spec from_binary(binary_map()) -> map(). +from_binary(<>) -> + riak_dt:from_binary(B). + + +%% =================================================================== +%% EUnit tests +%% =================================================================== +-ifdef(TEST). + +%% This fails on previous version of riak_dt_map +assoc_test() -> + Field = {'X', riak_dt_orswot}, + {ok, A} = update({update, [{update, Field, {add, 0}}]}, a, new()), + {ok, B} = update({update, [{update, Field, {add, 0}}]}, b, new()), + {ok, B2} = update({update, [{update, Field, {remove, 0}}]}, b, B), + C = A, + {ok, C3} = update({update, [{remove, Field}]}, c, C), + ?assertEqual(merge(A, merge(B2, C3)), merge(merge(A, B2), C3)), + ?assertEqual(value(merge(merge(A, C3), B2)), value(merge(merge(A, B2), C3))), + ?assertEqual(merge(merge(A, C3), B2), merge(merge(A, B2), C3)). + +clock_test() -> + Field = {'X', riak_dt_orswot}, + {ok, A} = update({update, [{update, Field, {add, 0}}]}, a, new()), + B = A, + {ok, B2} = update({update, [{update, Field, {add, 1}}]}, b, B), + {ok, A2} = update({update, [{update, Field, {remove, 0}}]}, a, A), + {ok, A3} = update({update, [{remove, Field}]}, a, A2), + {ok, A4} = update({update, [{update, Field, {add, 2}}]}, a, A3), + AB = merge(A4, B2), + ?assertEqual([{Field, [1, 2]}], value(AB)). + +remfield_test() -> + Field = {'X', riak_dt_orswot}, + {ok, A} = update({update, [{update, Field, {add, 0}}]}, a, new()), + B = A, + {ok, A2} = update({update, [{update, Field, {remove, 0}}]}, a, A), + {ok, A3} = update({update, [{remove, Field}]}, a, A2), + {ok, A4} = update({update, [{update, Field, {add, 2}}]}, a, A3), + AB = merge(A4, B), + ?assertEqual([{Field, [2]}], value(AB)). + +%% Bug found by EQC, not dropping dots in merge when an element is +%% present in both Maos leads to removed items remaining after merge. +present_but_removed_test() -> + F = {'X', riak_dt_lwwreg}, + %% Add Z to A + {ok, A} = update({update, [{update, F, {assign, <<"A">>}}]}, a, new()), + %% Replicate it to C so A has 'Z'->{a, 1} + C = A, + %% Remove Z from A + {ok, A2} = update({update, [{remove, F}]}, a, A), + %% Add Z to B, a new replica + {ok, B} = update({update, [{update, F, {assign, <<"B">>}}]}, b, new()), + %% Replicate B to A, so now A has a Z, the one with a Dot of + %% {b,1} and clock of [{a, 1}, {b, 1}] + A3 = merge(B, A2), + %% Remove the 'Z' from B replica + {ok, B2} = update({update, [{remove, F}]}, b, B), + %% Both C and A have a 'Z', but when they merge, there should be + %% no 'Z' as C's has been removed by A and A's has been removed by + %% C. + Merged = lists:foldl(fun(Set, Acc) -> + merge(Set, Acc) end, + %% the order matters, the two replicas that + %% have 'Z' need to merge first to provoke + %% the bug. You end up with 'Z' with two + %% dots, when really it should be removed. + A3, + [C, B2]), + ?assertEqual([], value(Merged)). + + +%% A bug EQC found where dropping the dots in merge was not enough if +%% you then store the value with an empty clock (derp). +no_dots_left_test() -> + F = {'Z', riak_dt_lwwreg}, + {ok, A} = update({update, [{update, F, {assign, <<"A">>}}]}, a, new()), + {ok, B} = update({update, [{update, F, {assign, <<"B">>}}]}, b, new()), + C = A, %% replicate A to empty C + {ok, A2} = update({update, [{remove, F}]}, a, A), + %% replicate B to A, now A has B's 'Z' + A3 = merge(A2, B), + %% Remove B's 'Z' + {ok, B2} = update({update, [{remove, F}]}, b, B), + %% Replicate C to B, now B has A's old 'Z' + B3 = merge(B2, C), + %% Merge everytyhing, without the fix You end up with 'Z' present, + %% with no dots + Merged = lists:foldl(fun(Set, Acc) -> + merge(Set, Acc) end, + A3, + [B3, C]), + ?assertEqual([], value(Merged)). + +%% A reset-remove bug eqc found where dropping a superseded dot lost +%% field remove merge information the dropped dot contained, adding +%% the tombstone fixed this. +tombstone_remove_test() -> + F = {'X', riak_dt_orswot}, + A=B=new(), + {ok, A1} = update({update, [{update, F, {add, 0}}]}, a, A), + %% Replicate! + B1 = merge(A1, B), + {ok, A2} = update({update, [{remove, F}]}, a, A1), + {ok, B2} = update({update, [{update, F, {add, 1}}]}, b, B1), + %% Replicate + A3 = merge(A2, B2), + %% that remove of F from A means remove the 0 A added to F + ?assertEqual([{F, [1]}], value(A3)), + {ok, B3} = update({update, [{update, F, {add, 2}}]}, b, B2), + %% replicate to A + A4 = merge(A3, B3), + %% final values + Final = merge(A4, B3), + %% before adding the tombstone, the dropped dots were simply + %% merged with the surviving field. When the second update to B + %% was merged with A, that information contained in the superseded + %% field in A at {b,1} was lost (since it was merged into the + %% _VALUE_). This casued the [0] from A's first dot to + %% resurface. By adding the tombstone, the superseded field merges + %% it's tombstone with the surviving {b, 2} field so the remove + %% information is preserved, even though the {b, 1} value is + %% dropped. Pro-tip, don't alter the CRDTs' values in the merge! + ?assertEqual([{F, [1,2]}], value(Final)). + +%% This test is a regression test for a counter example found by eqc. +%% The previous version of riak_dt_map used the `dot' from the field +%% update/creation event as key in `merge_left/3'. Of course multiple +%% fields can be added/updated at the same time. This means they get +%% the same `dot'. When merging two replicas, it is possible that one +%% has removed one or more of the fields added at a particular `dot', +%% which meant a function clause error in `merge_left/3'. The +%% structure was wrong, it didn't take into account the possibility +%% that multiple fields could have the same `dot', when clearly, they +%% can. This test fails with `dot' as the key for a field in +%% `merge_left/3', but passes with the current structure, of +%% `{field(), dot()}' as key. +dot_key_test() -> + {ok, A} = update({update, [{update, {'X', riak_dt_orswot}, {add, <<"a">>}}, {update, {'X', riak_dt_od_flag}, enable}]}, a, new()), + B = A, + {ok, A2} = update({update, [{remove, {'X', riak_dt_od_flag}}]}, a, A), + ?assertEqual([{{'X', riak_dt_orswot}, [<<"a">>]}], value(merge(B, A2))). + +stat_test() -> + Map = new(), + {ok, Map1} = update({update, [{update, {c, riak_dt_emcntr}, increment}, + {update, {s, riak_dt_orswot}, {add, <<"A">>}}, + {update, {m, riak_dt_map}, {update, [{update, {ss, riak_dt_orswot}, {add, 0}}]}}, + {update, {l, riak_dt_lwwreg}, {assign, <<"a">>, 1}}, + {update, {l2, riak_dt_lwwreg}, {assign, <<"b">>, 2}}]}, a1, Map), + {ok, Map2} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"foo">>, 3}}]}, a2, Map1), + {ok, Map3} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"bar">>, 4}}]}, a3, Map1), + Map4 = merge(Map2, Map3), + ?assertEqual([{actor_count, 0}, {field_count, 0}, {duplication, 0}, {deferred_length, 0}], stats(Map)), + ?assertEqual(3, stat(actor_count, Map4)), + ?assertEqual(5, stat(field_count, Map4)), + ?assertEqual(undefined, stat(waste_pct, Map4)), + ?assertEqual(1, stat(duplication, Map4)), + {ok, Map5} = update({update, [{update, {l3, riak_dt_lwwreg}, {assign, <<"baz">>, 5}}]}, a3, Map4), + ?assertEqual(6, stat(field_count, Map5)), + ?assertEqual(1, stat(duplication, Map5)), + %% Updating field {l, riak_dt_lwwreg} merges the duplicates to a single field + %% @see apply_ops + {ok, Map6} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"bim">>, 6}}]}, a2, Map5), + ?assertEqual(0, stat(duplication, Map6)), + {ok, Map7} = update({update, [{remove, {l, riak_dt_lwwreg}}]}, a1, Map6), + ?assertEqual(5, stat(field_count, Map7)). + +equals_test() -> + {ok, A} = update({update, [{update, {'X', riak_dt_orswot}, {add, <<"a">>}}, {update, {'X', riak_dt_od_flag}, enable}]}, a, new()), + {ok, B} = update({update, [{update, {'Y', riak_dt_orswot}, {add, <<"a">>}}, {update, {'Z', riak_dt_od_flag}, enable}]}, b, new()), + ?assert(not equal(A, B)), + C = merge(A, B), + D = merge(B, A), + ?assert(equal(C, D)), + ?assert(equal(A, A)). + +-ifdef(EQC). +-define(NUMTESTS, 1000). + +%% =================================== +%% crdt_statem_eqc callbacks +%% =================================== +size(Map) -> + %% How big is a Map? Maybe number of fields and depth matter? But + %% then the number of fields in sub maps too? + byte_size(term_to_binary(Map)) div 10. + +generate() -> + ?LET({Ops, Actors}, {non_empty(list(gen_op())), non_empty(list(bitstring(16*8)))}, + lists:foldl(fun(Op, Map) -> + Actor = case length(Actors) of + 1 -> hd(Actors); + _ -> lists:nth(crypto:rand_uniform(1, length(Actors)), Actors) + end, + case update(Op, Actor, Map) of + {ok, M} -> M; + _ -> Map + end + end, + new(), + Ops)). + +%% Add depth parameter +gen_op() -> + ?SIZED(Size, gen_op(Size)). + +gen_op(Size) -> + ?LET(Ops, non_empty(list(gen_update(Size))), {update, Ops}). + +gen_update(Size) -> + ?LET(Field, gen_field(Size), + oneof([{remove, Field}, + {update, Field, gen_field_op(Field, Size div 2)}])). + +gen_field() -> + ?SIZED(Size, gen_field(Size)). + +gen_field(Size) -> + {growingelements(['A', 'B', 'C', 'X', 'Y', 'Z']) %% Macro? Bigger? + , elements([ + riak_dt_emcntr, + riak_dt_orswot, +%% riak_dt_lwwreg, + riak_dt_od_flag + ] ++ [?MODULE || Size > 0])}. + +gen_field_op({_Name, Type}, Size) -> + Type:gen_op(Size). + + +-endif. + +-endif. diff --git a/test/map2_eqc.erl b/test/map2_eqc.erl new file mode 100644 index 0000000..07a4043 --- /dev/null +++ b/test/map2_eqc.erl @@ -0,0 +1,685 @@ +%% ------------------------------------------------------------------- +%% +%% map_eqc: Drive out the merge bugs the other statem couldn't +%% +%% Copyright (c) 2007-2014 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(map_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,{replicas=[], + %% a unique tag per add + counter=1 :: pos_integer(), + %% fields that have been added + adds=[] :: [{atom(), module()}] + }). + +-define(NUMTESTS, 1000). +-define(QC_OUT(P), + eqc:on_output(fun(Str, Args) -> + io:format(user, Str, Args) end, P)). +%% @doc eunit runner +eqc_test_() -> + {timeout, 200, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(100, ?QC_OUT(prop_merge()))))}. + +%% @doc eunit runner for bin roundtrip +bin_roundtrip_test_() -> + {timeout, 100, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(50, ?QC_OUT(crdt_statem_eqc:prop_bin_roundtrip(riak_dt_map)))))}. + +%% @doc shell convenience, run eqc property 1000 times. +run() -> + run(?NUMTESTS). + +%% @doc shell convenience, run eqc property `Count' times +run(Count) -> + eqc:quickcheck(eqc:numtests(Count, prop_merge())). + +%% @doc shell convenience, check eqc property (last failing counter example) +check() -> + eqc:check(prop_merge()). + +%% Initialize the state +-spec initial_state() -> eqc_statem:symbolic_state(). +initial_state() -> + #state{}. + +%% ------ Grouped operator: create_replica +create_replica_pre(#state{replicas=Replicas}) -> + length(Replicas) < 10. + +%% @doc create_replica_arge - Generate a replica +create_replica_args(_S) -> + %% don't waste time shrinking actor id binaries + [noshrink(binary(8))]. + +%% @doc create_replica_pre - Don't duplicate replicas +-spec create_replica_pre(S :: eqc_statem:symbolic_state(), + Args :: [term()]) -> boolean(). +create_replica_pre(#state{replicas=Replicas}, [Id]) -> + not lists:member(Id, Replicas). + +%% @doc store new replica ID and bottom map/model in ets +create_replica(Id) -> + ets:insert(map_eqc, {Id, riak_dt_map:new(), model_new()}). + +%% @doc create_replica_next - Add replica ID to state +-spec create_replica_next(S :: eqc_statem:symbolic_state(), + V :: eqc_statem:var(), + Args :: [term()]) -> eqc_statem:symbolic_state(). +create_replica_next(S=#state{replicas=R0}, _Value, [Id]) -> + S#state{replicas=R0++[Id]}. + + +%% ------ Grouped operator: remove +%% @doc remove, but only something that has been added already +remove_pre(#state{replicas=Replicas, adds=Adds}) -> + Replicas /= [] andalso Adds /= []. + +%% @doc remove something that has been added already from any replica +remove_args(#state{adds=Adds, replicas=Replicas}) -> + [ + elements(Replicas), + elements(Adds) %% A Field that has been added + ]. + +%% @doc correct shrinking +remove_pre(#state{replicas=Replicas}, [Replica, _]) -> + lists:member(Replica, Replicas). + +%% @doc perform a remove operation, remove `Field' from Map/Model at +%% `Replica' +remove(Replica, Field) -> + [{Replica, Map, Model}] = ets:lookup(map_eqc, Replica), + %% even though we only remove what has been added, there is no + %% guarantee a merge from another replica hasn't led to the + %% Field being removed already, so ignore precon errors (they + %% don't change state) + {ok, Map2} = ignore_precon_error(riak_dt_map:update({update, [{remove, Field}]}, Replica, Map), Map), + Model2 = model_remove_field(Field, Model), + ets:insert(map_eqc, {Replica, Map2, Model2}), + {Map2, Model2}. + +%% @doc remove post condition, @see post_all/2 +remove_post(_S, [_Replica, Field], Res) -> + post_all(Res, remove) andalso field_not_present(Field, Res). + +%% ------ Grouped operator: ctx_remove +%% @doc remove, but with a context +ctx_remove_pre(#state{replicas=Replicas, adds=Adds}) -> + Replicas /= [] andalso Adds /= []. + +%% @doc generate ctx rmeove args +ctx_remove_args(#state{replicas=Replicas, adds=Adds}) -> + [ + elements(Replicas), %% read from + elements(Replicas), %% send op to + elements(Adds) %% which field to remove + ]. + +%% @doc ensure correct shrinking +ctx_remove_pre(#state{replicas=Replicas, adds=Adds}, [From, To, Field]) -> + lists:member(From, Replicas) andalso lists:member(To, Replicas) + andalso lists:member(Field, Adds). + +%% @doc dynamic precondition, only context remove if the `Field' is in +%% the `From' replicas +ctx_remove_dynamicpre(_S, [From, _To, Field]) -> + [{From, Map, _Model}] = ets:lookup(map_eqc, From), + lists:keymember(Field, 1, riak_dt_map:value(Map)). + +%% @doc perform a context remove on the map and model using context +%% from `From' +ctx_remove(From, To, Field) -> + [{From, FromMap, FromModel}] = ets:lookup(map_eqc, From), + [{To, ToMap, ToModel}] = ets:lookup(map_eqc, To), + Ctx = riak_dt_map:precondition_context(FromMap), + {ok, Map} = riak_dt_map:update({update, [{remove, Field}]}, To, ToMap, Ctx), + Model = model_ctx_remove(Field, FromModel, ToModel), + ets:insert(map_eqc, {To, Map, Model}), + {Map, Model}. + +%% @doc @see post_all/2 +ctx_remove_post(_S, _Args, Res) -> + post_all(Res, ctx_remove). + +%% ------ Grouped operator: replicate Merges two replicas' values. + +%% @doc must be a replica at least. +replicate_pre(#state{replicas=Replicas}) -> + Replicas /= []. + +%% @doc chose from/to replicas, can be the same +replicate_args(#state{replicas=Replicas}) -> + [ + elements(Replicas), %% Replicate from + elements(Replicas) %% Replicate to + ]. + +%% @doc replicate_pre - shrink correctly +-spec replicate_pre(S :: eqc_statem:symbolic_state(), + Args :: [term()]) -> boolean(). +replicate_pre(#state{replicas=Replicas}, [From, To]) -> + lists:member(From, Replicas) andalso lists:member(To, Replicas). + +%% @doc Replicate a CRDT from `From' to `To' +replicate(From, To) -> + [{From, FromMap, FromModel}] = ets:lookup(map_eqc, From), + [{To, ToMap, ToModel}] = ets:lookup(map_eqc, To), + + Map = riak_dt_map:merge(FromMap, ToMap), + Model = model_merge(FromModel, ToModel), + + ets:insert(map_eqc, {To, Map, Model}), + {Map, Model}. + +%% @doc @see post_all/2 +replicate_post(_S, _Args, Res) -> + post_all(Res, rep). + +%% ------ Grouped operator: ctx_update +%% Update a Field in the Map, using the Map context + +%% @doc there must be at least one replica +ctx_update_pre(#state{replicas=Replicas}) -> + Replicas /= []. + +%% @doc generate an operation +ctx_update_args(#state{replicas=Replicas, counter=Cnt}) -> + ?LET({Field, Op}, gen_field_and_op(), + [ + Field, + Op, + elements(Replicas), + elements(Replicas), + Cnt + ]). + +%% @doc ensure correct shrinking +ctx_update_pre(#state{replicas=Replicas}, [_Field, _Op, From, To, _Cnt]) -> + lists:member(From, Replicas) andalso lists:member(To, Replicas). + +%% @doc much like context_remove, get a contet from `From' and apply +%% `Op' at `To' +ctx_update(Field, Op, From, To, Cnt) -> + [{From, CtxMap, CtxModel}] = ets:lookup(map_eqc, From), + [{To, ToMap, ToModel}] = ets:lookup(map_eqc, To), + + Ctx = riak_dt_map:precondition_context(CtxMap), + ModCtx = model_ctx(CtxModel), + {ok, Map} = riak_dt_map:update({update, [{update, Field, Op}]}, To, ToMap, Ctx), + {ok, Model} = model_update_field(Field, Op, To, Cnt, ToModel, ModCtx), + + ets:insert(map_eqc, {To, Map, Model}), + {Map, Model}. + +%% @doc update the model state, incrementing the counter represents logical time. +ctx_update_next(S=#state{counter=Cnt, adds=Adds}, _Res, [Field, _Op, _From, _To, _Cnt]) -> + S#state{adds=lists:umerge(Adds, [Field]), counter=Cnt+1}. + +%% @doc @see post_all/2 +ctx_update_post(_S, _Args, Res) -> + post_all(Res, update). + +%% ------ Grouped operator: update +%% Update a Field in the Map + +%% @doc there must be at least one replica +update_pre(#state{replicas=Replicas}) -> + Replicas /= []. + +%% @doc choose a field, operation and replica +update_args(#state{replicas=Replicas, counter=Cnt}) -> + ?LET({Field, Op}, gen_field_and_op(), + [ + Field, + Op, + elements(Replicas), + Cnt + ]). + +%% @doc shrink correctly +update_pre(#state{replicas=Replicas}, [_Field, _Op, Replica, _Cnt]) -> + lists:member(Replica, Replicas). + +%% @doc apply `Op' to `Field' at `Replica' +update(Field, Op, Replica, Cnt) -> + [{Replica, Map0, Model0}] = ets:lookup(map_eqc, Replica), + + {ok, Map} = ignore_precon_error(riak_dt_map:update({update, [{update, Field, Op}]}, Replica, Map0), Map0), + {ok, Model} = model_update_field(Field, Op, Replica, Cnt, Model0, undefined), + + ets:insert(map_eqc, {Replica, Map, Model}), + {Map, Model}. + +%% @doc increment the time counter, and add this field to adds as a +%% candidate to be removed later. +update_next(S=#state{counter=Cnt, adds=Adds}, _Res, [Field, _, _, _]) -> + S#state{adds=lists:umerge(Adds, [Field]), counter=Cnt+1}. + +%% @doc @see post_all/2 +update_post(_S, _Args, Res) -> + post_all(Res, update). + +%% ------ Grouped operator: idempotent + +idempotent_args(#state{replicas=Replicas}) -> + [elements(Replicas)]. + +%% @doc idempotent_pre - Precondition for generation +-spec idempotent_pre(S :: eqc_statem:symbolic_state()) -> boolean(). +idempotent_pre(#state{replicas=Replicas}) -> + Replicas /= []. + +%% @doc idempotent_pre - Precondition for idempotent +-spec idempotent_pre(S :: eqc_statem:symbolic_state(), + Args :: [term()]) -> boolean(). +idempotent_pre(#state{replicas=Replicas}, [Replica]) -> + lists:member(Replica, Replicas). + +%% @doc idempotent - Merge replica with itself, result used for post condition only +idempotent(Replica) -> + [{Replica, Map, _Model}] = ets:lookup(map_eqc, Replica), + {Map, riak_dt_map:merge(Map, Map)}. + +%% @doc idempotent_post - Postcondition for idempotent +-spec idempotent_post(S :: eqc_statem:dynamic_state(), + Args :: [term()], R :: term()) -> true | term(). +idempotent_post(_S, [_Replica], {Map, MergedSelfMap}) -> + riak_dt_map:equal(Map, MergedSelfMap). + +%% ------ Grouped operator: commutative + +commutative_args(#state{replicas=Replicas}) -> + [elements(Replicas), elements(Replicas)]. + +%% @doc commutative_pre - Precondition for generation +-spec commutative_pre(S :: eqc_statem:symbolic_state()) -> boolean(). +commutative_pre(#state{replicas=Replicas}) -> + Replicas /= []. + +%% @doc commutative_pre - Precondition for commutative +-spec commutative_pre(S :: eqc_statem:symbolic_state(), + Args :: [term()]) -> boolean(). +commutative_pre(#state{replicas=Replicas}, [Replica, Replica2]) -> + lists:member(Replica, Replicas) andalso lists:member(Replica2, Replicas). + +%% @doc commutative - Merge maps both ways (result used for post condition) +commutative(Replica1, Replica2) -> + [{Replica1, Map1, _Model1}] = ets:lookup(map_eqc, Replica1), + [{Replica2, Map2, _Model2}] = ets:lookup(map_eqc, Replica2), + {riak_dt_map:merge(Map1, Map2), riak_dt_map:merge(Map2, Map1)}. + +%% @doc commutative_post - Postcondition for commutative +-spec commutative_post(S :: eqc_statem:dynamic_state(), + Args :: [term()], R :: term()) -> true | term(). +commutative_post(_S, [_Replica1, _Replica2], {OneMergeTwo, TwoMergeOne}) -> + riak_dt_map:equal(OneMergeTwo, TwoMergeOne). + +%% ------ Grouped operator: associative + +associative_args(#state{replicas=Replicas}) -> + [elements(Replicas), elements(Replicas), elements(Replicas)]. + +%% @doc associative_pre - Precondition for generation +-spec associative_pre(S :: eqc_statem:symbolic_state()) -> boolean(). +associative_pre(#state{replicas=Replicas}) -> + Replicas /= []. + +%% @doc associative_pre - Precondition for associative +-spec associative_pre(S :: eqc_statem:symbolic_state(), + Args :: [term()]) -> boolean(). +associative_pre(#state{replicas=Replicas}, [Replica, Replica2, Replica3]) -> + lists:member(Replica, Replicas) + andalso lists:member(Replica2, Replicas) + andalso lists:member(Replica3, Replicas). + +%% @doc associative - Merge maps three ways (result used for post condition) +associative(Replica1, Replica2, Replica3) -> + [{Replica1, Map1, _Model1}] = ets:lookup(map_eqc, Replica1), + [{Replica2, Map2, _Model2}] = ets:lookup(map_eqc, Replica2), + [{Replica3, Map3, _Model3}] = ets:lookup(map_eqc, Replica3), + {riak_dt_map:merge(riak_dt_map:merge(Map1, Map2), Map3), + riak_dt_map:merge(riak_dt_map:merge(Map1, Map3), Map2), + riak_dt_map:merge(riak_dt_map:merge(Map2, Map3), Map1)}. + +%% @doc associative_post - Postcondition for associative +-spec associative_post(S :: eqc_statem:dynamic_state(), + Args :: [term()], R :: term()) -> true | term(). +associative_post(_S, [_Replica1, _Replica2, _Replica3], {ABC, ACB, BCA}) -> +%% case {riak_dt_map:equal(ABC, ACB), riak_dt_map:equal(ACB, BCA)} of + case {map_values_equal(ABC, ACB), map_values_equal(ACB, BCA)} of + {true, true} -> + true; + {false, true} -> + {postcondition_failed, {ACB, not_associative, ABC}}; + {true, false} -> + {postcondition_failed, {ACB, not_associative, BCA}}; + {false, false} -> + {postcondition_failed, {{ACB, not_associative, ABC}, '&&', {ACB, not_associative, BCA}}} + end. + +map_values_equal(Map1, Map2) -> + lists:sort(riak_dt_map:value(Map1)) == lists:sort(riak_dt_map:value(Map2)). + +%% @Doc Weights for commands. Don't create too many replicas, but +%% prejudice in favour of creating more than 1. Try and balance +%% removes with adds. But favour adds so we have something to +%% remove. See the aggregation output. +weight(S, create_replica) when length(S#state.replicas) > 2 -> + 1; +weight(S, create_replica) when length(S#state.replicas) < 5 -> + 4; +weight(_S, remove) -> + 2; +weight(_S, context_remove) -> + 5; +weight(_S, update) -> + 3; +weight(_S, ctx_update) -> + 3; +weight(_S, _) -> + 1. + + +%% @doc Tests the property that a riak_dt_map is equivalent to the Map +%% Model. The Map Model is based roughly on an or-set design. Inspired +%% by a draft spec in sent in private email by Carlos Baquero. The +%% model extends the spec to onclude context operations, deferred +%% operations, and reset-remove semantic (with tombstones.) +prop_merge() -> + ?FORALL(Cmds, commands(?MODULE), + begin + %% store state in eqc, external to statem state + ets:new(map_eqc, [named_table, set]), + {H, S, Res} = run_commands(?MODULE,Cmds), + ReplicaData = ets:tab2list(map_eqc), + %% Check that merging all values leads to the same results for Map and the Model + {Map, Model} = lists:foldl(fun({_Actor, InMap, InModel}, {M, Mo}) -> + {riak_dt_map:merge(M, InMap), + model_merge(Mo, InModel)} + end, + {riak_dt_map:new(), model_new()}, + ReplicaData), + MapValue = riak_dt_map:value(Map), + ModelValue = model_value(Model), + %% clean up + ets:delete(map_eqc), + %% prop + pretty_commands(?MODULE, Cmds, {H, S, Res}, + measure(actors, length(ReplicaData), + measure(length, length(MapValue), + measure(depth, map_depth(MapValue), + aggregate(command_names(Cmds), + conjunction([{results, equals(Res, ok)}, + {value, equals(lists:sort(MapValue), lists:sort(ModelValue))} + ]) + ))))) + end). + +%% ----------- +%% Generators +%% ---------- + +%% @doc Keep the number of possible field names down to a minimum. The +%% smaller state space makes EQC more likely to find bugs since there +%% will be more action on the fields. Learned this from +%% crdt_statem_eqc having to large a state space and missing bugs. +gen_field() -> + {growingelements(['A', 'B', 'C', 'D', 'X', 'Y', 'Z']), + elements([ + riak_dt_orswot, + riak_dt_emcntr, + riak_dt_lwwreg, + riak_dt_map, + riak_dt_od_flag + ])}. + +%% @use the generated field to generate an op. Delegates to type. Some +%% Type generators are recursive, pass in Size to limit the depth of +%% recusrions. @see riak_dt_map:gen_op/1. +gen_field_op({_Name, Type}) -> + ?SIZED(Size, Type:gen_op(Size)). + +%% @doc geneate a field, and a valid operation for the field +gen_field_and_op() -> + ?LET(Field, gen_field(), {Field, gen_field_op(Field)}). + + +%% ----------- +%% Helpers +%% ---------- + +%% @doc how deeply nested is `Map'? Recurse down the Map and return +%% the deepest nesting. A depth of `1' means only a top-level Map. `2' +%% means a map in the seocnd level, `3' in the third, and so on. +map_depth(Map) -> + map_depth(Map, 1). + +%% @doc iterate a maps fields, and recurse down map fields to get a +%% max depth. +map_depth(Map, D) -> + lists:foldl(fun({{_, riak_dt_map}, SubMap}, MaxDepth) -> + Depth = map_depth(SubMap, D+1), + max(MaxDepth, Depth); + (_, Depth) -> + Depth + end, + D, + Map). + +%% @doc precondition errors don't change the state of a map, so ignore +%% them. +ignore_precon_error({ok, NewMap}, _) -> + {ok, NewMap}; +ignore_precon_error(_, Map) -> + {ok, Map}. + +%% @doc for all mutating operations enusre that the state at the +%% mutated replica is equal for the map and the model +post_all({Map, Model}, Cmd) -> + %% What matters is that both types have the exact same results. + case lists:sort(riak_dt_map:value(Map)) == lists:sort(model_value(Model)) of + true -> + true; + _ -> + {postcondition_failed, "Map and Model don't match", Cmd, Map, Model, riak_dt_map:value(Map), model_value(Model)} + end. + +%% @doc `true' if `Field' is not in `Map' +field_not_present(Field, {Map, _Model}) -> + case lists:keymember(Field, 1, riak_dt_map:value(Map)) of + false -> + true; + true -> + {Field, present, Map} + end. + +%% ----------- +%% Model +%% ---------- + +%% The model has to be a Map CRDT, with the same deferred operations +%% and reset-remove semantics as riak_dt_map. Luckily it has no +%% efficiency constraints. It's modelled as three lists. Added fields, +%% removed fields, and deferred remove operations. There is also an +%% entry for tombstones, and vclock. Whenever a field is removed, an +%% empty CRDT of that fields type is stored, with the clock at removal +%% time, in tombstones. Merging a fields value with this tombstone +%% ensures reset-remove semantics. The vector clock entry is only for +%% tombstones and context operations. The add/remove sets use a unique +%% tag, like in OR-Set, for elements. + +-record(model, { + %% Things added to the Map, a Set really, but uses a list + %% for ease of reading in case of a counter example + adds=[], + %% Tombstones of things removed from the map + removes=[], + %% Removes that are waiting for adds before they + %% can be run (like context ops in the + %% riak_dt_map) + deferred=[], + %% For reset-remove semantic, the field+clock at time of + %% removal + tombstones=orddict:new() :: orddict:orddict(), + %% for embedded context operations + clock=riak_dt_vclock:fresh() :: riak_dt_vclock:vclock() + }). + +-type map_model() :: #model{}. + +%% @doc create a new model. This is the bottom element. +-spec model_new() -> map_model(). +model_new() -> + #model{}. + +%% @doc update `Field' with `Op', by `Actor' at time `Cnt'. very +%% similar to riak_dt_map. +-spec model_update_field({atom(), module()}, term(), binary(), pos_integer(), + map_model(), undefined | riak_dt_vclock:vclock()) -> map_model(). +model_update_field({_Name, Type}=Field, Op, Actor, Cnt, Model, Ctx) -> + #model{adds=Adds, removes=Removes, clock=Clock, tombstones=TS} = Model, + %% generate a new clock + Clock2 = riak_dt_vclock:merge([[{Actor, Cnt}], Clock]), + %% Get those fields that are present, that is, only in the Adds + %% set + InMap = lists:subtract(Adds, Removes), + %% Merge all present values to get a current CRDT, and add each + %% field entry we merge to a set so we can move them to `removed' + {CRDT0, ToRem} = lists:foldl(fun({F, Value, _X}=E, {CAcc, RAcc}) when F == Field -> + {Type:merge(CAcc, Value), lists:umerge([E], RAcc)}; + (_, Acc) -> Acc + end, + {Type:new(), []}, + InMap), + %% If we have a tombstone for this field, merge with it to ensure + %% reset-remove + CRDT1 = case orddict:find(Field, TS) of + error -> + CRDT0; + {ok, TSVal} -> + Type:merge(CRDT0, TSVal) + end, + %% Update the clock to ensure a shared causal context + CRDT = Type:parent_clock(Clock2, CRDT1), + %% Apply the operation + case Type:update(Op, {Actor, Cnt}, CRDT, Ctx) of + {ok, Updated} -> + %% Op succeded, store the new value as the field, using + %% `Cnt' (which is time since the statem acts serially) as + %% a unique tag. + Model2 = Model#model{adds=lists:umerge([{Field, Updated, Cnt}], Adds), + removes=lists:umerge(ToRem, Removes), + clock=Clock2}, + {ok, Model2}; + _ -> + %% if the op failed just return the state un changed + {ok, Model} + end. + +%% @doc remove a field from the model, doesn't enforce a precondition, +%% removing a non-present field does nothing. +-spec model_remove_field({atom(), module()}, map_model()) -> map_model(). +model_remove_field({_Name, Type}=Field, Model) -> + #model{adds=Adds, removes=Removes, tombstones=Tombstones0, clock=Clock} = Model, + ToRemove = [{F, Val, Token} || {F, Val, Token} <- Adds, F == Field], + TS = Type:parent_clock(Clock, Type:new()), + Tombstones = orddict:update(Field, fun(T) -> Type:merge(TS, T) end, TS, Tombstones0), + Model#model{removes=lists:umerge(Removes, ToRemove), tombstones=Tombstones}. + +%% @doc merge two models. Very simple since the model truley always +%% grows, it is just a union of states, and then applies the deferred +%% operations. +-spec model_merge(map_model(), map_model()) -> map_model(). +model_merge(Model1, Model2) -> + #model{adds=Adds1, removes=Removes1, deferred=Deferred1, clock=Clock1, tombstones=TS1} = Model1, + #model{adds=Adds2, removes=Removes2, deferred=Deferred2, clock=Clock2, tombstones=TS2} = Model2, + Clock = riak_dt_vclock:merge([Clock1, Clock2]), + Adds0 = lists:umerge(Adds1, Adds2), + Tombstones = orddict:merge(fun({_Name, Type}, V1, V2) -> Type:merge(V1, V2) end, TS1, TS2), + Removes0 = lists:umerge(Removes1, Removes2), + Deferred0 = lists:umerge(Deferred1, Deferred2), + {Adds, Removes, Deferred} = model_apply_deferred(Adds0, Removes0, Deferred0), + #model{adds=Adds, removes=Removes, deferred=Deferred, clock=Clock, tombstones=Tombstones}. + +%% @doc apply deferred operations that may be relevant now a merge as taken place. +-spec model_apply_deferred(list(), list(), list()) -> {list(), list(), list()}. +model_apply_deferred(Adds, Removes, Deferred) -> + D2 = lists:subtract(Deferred, Adds), + ToRem = lists:subtract(Deferred, D2), + {Adds, lists:umerge(ToRem, Removes), D2}. + +%% @doc remove `Field' from `To' using `From's context. +-spec model_ctx_remove({atom(), module()}, map_model(), map_model()) -> map_model(). +model_ctx_remove({_N, Type}=Field, From, To) -> + %% get adds for Field, any adds for field in ToAdds that are in + %% FromAdds should be removed any others, put in deferred + #model{adds=FromAdds, clock=FromClock} = From, + #model{adds=ToAdds, removes=ToRemoves, deferred=ToDeferred, tombstones=TS} = To, + ToRemove = lists:filter(fun({F, _Val, _Token}) -> F == Field end, FromAdds), + Defer = lists:subtract(ToRemove, ToAdds), + Remove = lists:subtract(ToRemove, Defer), + Tombstone = Type:parent_clock(FromClock, Type:new()), + TS2 = orddict:update(Field, fun(T) -> Type:merge(T, Tombstone) end, Tombstone, TS), + To#model{removes=lists:umerge(Remove, ToRemoves), + deferred=lists:umerge(Defer, ToDeferred), + tombstones=TS2}. + +%% @doc get the actual value for a model +-spec model_value(map_model()) -> + [{Field::{atom(), module()}, Value::term()}]. +model_value(Model) -> + #model{adds=Adds, removes=Removes, tombstones=TS} = Model, + Remaining = lists:subtract(Adds, Removes), + %% fold over fields that are in the map and merge each to a merge + %% CRDT per field + Res = lists:foldl(fun({{_Name, Type}=Key, Value, _X}, Acc) -> + %% if key is in Acc merge with it and replace + dict:update(Key, fun(V) -> + Type:merge(V, Value) end, + Value, Acc) end, + dict:new(), + Remaining), + %% fold over merged CRDTs and merge with the tombstone (if any) + %% for each field + Res2 = dict:fold(fun({_N, Type}=Field, Val, Acc) -> + case orddict:find(Field, TS) of + error -> + dict:store(Field, Val, Acc); + {ok, TSVal} -> + dict:store(Field, Type:merge(TSVal, Val), Acc) + end + end, + dict:new(), + Res), + %% Call `value/1' on each CRDT in the model + [{K, Type:value(V)} || {{_Name, Type}=K, V} <- dict:to_list(Res2)]. + +%% @doc get a context for a model context operation +-spec model_ctx(map_model()) -> riak_dt_vclock:vclock(). +model_ctx(#model{clock=Ctx}) -> + Ctx. + + +-endif. % EQC From 1462e3dd36f10a2e0952b1488eb098fd7f893b7d Mon Sep 17 00:00:00 2001 From: Russell Brown Date: Mon, 8 Dec 2014 16:40:40 +0000 Subject: [PATCH 02/33] WIP smaller map, doesn't keep duplicate fields (merges on merge) Also takes care to not duplicate lots of clocks throughout structure. Still uses dots at head of field to solve the bottom vs. bottomish thing --- src/riak_dt_map.erl | 297 +++++----------- src/riak_dt_map2.erl | 807 ------------------------------------------- test/map2_eqc.erl | 685 ------------------------------------ 3 files changed, 83 insertions(+), 1706 deletions(-) delete mode 100644 src/riak_dt_map2.erl delete mode 100644 test/map2_eqc.erl diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 8e970d4..bf157b6 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -191,24 +191,21 @@ -type binary_map() :: binary(). %% A binary that from_binary/1 will accept -type map() :: {riak_dt_vclock:vclock(), entries(), deferred()}. --type entries() :: dict(field_name(), field_value()). +-type entries() :: [field()]. -type field() :: {field_name(), field_value()}. -type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. --type field_value() :: {crdts(), tombstone()}. +-type field_value() :: {riak_dt_vclock:vclock(), crdt()}. --type crdts() :: [entry()]. --type entry() :: {riak_dt:dot(), crdt()}. +%%-type crdts() :: [entry()]. +%%-type entry() :: {riak_dt:dot(), crdt()}. %% Only for present fields, ensures removes propogate --type tombstone() :: crdt(). +%%-type tombstone() :: crdt(). %% Only field removals can be deferred. CRDTs stored in the map may %% have contexts and deferred operations, but as these are part of the %% state, they are stored under the field as an update like any other. --type deferred() :: dict(context(), [field()]). - -%% used until we move to erlang 17 and can use dict:dict/2 --type dict(_A, _B) :: dict(). +-type deferred() :: [{context(), [field()]}]. %% limited to only those mods that support both a shared causal %% context, and by extension, the reset-remove semantic. @@ -237,12 +234,13 @@ -type value() :: {field(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. -type precondition_error() :: {error, {precondition, {not_present, field()}}}. +-define(FRESH_CLOCK, riak_dt_vclock:fresh()). -define(DICT, dict). %% @doc Create a new, empty Map. -spec new() -> map(). new() -> - {riak_dt_vclock:fresh(), ?DICT:new(), ?DICT:new()}. + {riak_dt_vclock:fresh(), dict:new(), dict:new()}. %% @doc sets the clock in the map to that `Clock'. Used by a %% containing Map for sub-CRDTs @@ -253,28 +251,10 @@ parent_clock(Clock, {_MapClock, Values, Deferred}) -> %% @doc get the current set of values for this Map -spec value(map()) -> values(). value({_Clock, Values, _Deferred}) -> - lists:sort(?DICT:fold(fun({Name, Type}, CRDTs, Acc) -> - Merged = merge_crdts(Type, CRDTs), - [{{Name, Type}, Type:value(Merged)} | Acc] end, - [], - Values)). - -%% @private merge entry for field, if present, or return new if not -merge_field({_Name, Type}, error) -> - Type:new(); -merge_field({_Name, Type}, {ok, CRDTs}) -> - merge_crdts(Type, CRDTs); -merge_field(Field, Values) -> - merge_field(Field, ?DICT:find(Field, Values)). - -%% @private merge the CRDTs of a type -merge_crdts(Type, {CRDTs, TS}) -> - V = ?DICT:fold(fun(_Dot, CRDT, CRDT0) -> - Type:merge(CRDT0, CRDT) end, - Type:new(), - CRDTs), - %% Merge with the tombstone to drop any removed dots - Type:merge(TS, V). + lists:sort(dict:fold(fun({Name, Type}, {_Dots, CRDT}, Acc) -> + [{{Name, Type}, Type:value(CRDT)} | Acc] end, + [], + Values)). %% @doc query map (not implemented yet) -spec value(term(), map()) -> values(). @@ -327,6 +307,24 @@ update_clock(Actor, Clock) -> Dot = {Actor, riak_dt_vclock:get_counter(Actor, NewClock)}, {Dot, NewClock}. +get({_Name, Type}=Field, Fields, Clock) -> + CRDT = case dict:find(Field, Fields) of + {ok, {_Dots, CRDT0}} -> + CRDT0; + error -> + Type:new() + end, + Type:parent_clock(Clock, CRDT). + +get_entry({_Name, Type}=Field, Fields, Clock) -> + {Dots, CRDT} = case dict:find(Field, Fields) of + {ok, Entry} -> + Entry; + error -> + {[], Type:new()} + end, + {Dots, Type:parent_clock(Clock, CRDT)}. + %% @private -spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), {riak_dt_vclock:vclock(), entries() , deferred()}, context()) -> @@ -334,17 +332,11 @@ update_clock(Actor, Clock) -> apply_ops([], _Dot, Map, _Ctx) -> {ok, Map}; apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> - CRDT = merge_field(Field, Values), - CRDT1 = Type:parent_clock(Clock, CRDT), - case Type:update(Op, Dot, CRDT1, Ctx) of - {ok, Updated} -> - NewValues = ?DICT:store(Field, {?DICT:store(Dot, Updated, ?DICT:new()), - %% old tombstone was - %% merged into current - %% value so create a new - %% empty one - Type:new()} - , Values), + CRDT = get(Field, Values, Clock), + case Type:update(Op, Dot, CRDT, Ctx) of + {ok, Updated0} -> + Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), + NewValues = dict:store(Field, {[Dot], Updated}, Values), apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); Error -> Error @@ -372,27 +364,27 @@ apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> -spec remove_field(field(), map(), context()) -> {ok, map()} | precondition_error(). remove_field(Field, {Clock, Values, Deferred}, undefined) -> - case ?DICT:find(Field, Values) of + case dict:find(Field, Values) of error -> {error, {precondition, {not_present, Field}}}; {ok, _Removed} -> - {ok, {Clock, ?DICT:erase(Field, Values), Deferred}} + {ok, {Clock, dict:erase(Field, Values), Deferred}} end; %% Context removes remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), NewValues = case ctx_rem_field(Field, Values, Ctx, Clock) of empty -> - ?DICT:erase(Field, Values); - CRDTs -> - ?DICT:store(Field, CRDTs, Values) + dict:erase(Field, Values); + CRDT -> + dict:store(Field, CRDT, Values) end, {ok, {Clock, NewValues, Deferred}}. %% @private drop dominated fields ctx_rem_field(_Field, error, _Ctx_, _Clock) -> empty; -ctx_rem_field({_, Type}, {ok, {CRDTs, TS0}}, Ctx, MapClock) -> +ctx_rem_field({_, Type}, {ok, {Dots, CRDT}}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% %% If the context is removing a field at dot {a, 1} and the @@ -403,19 +395,18 @@ ctx_rem_field({_, Type}, {ok, {CRDTs, TS0}}, Ctx, MapClock) -> %% TombstoneClock = riak_dt_vclock:glb(Ctx, MapClock), %% GLB is events seen by both clocks only TS = Type:parent_clock(TombstoneClock, Type:new()), - Remaining = ?DICT:filter(fun(Dot, _CRDT) -> - is_dot_unseen(Dot, Ctx) - end, - CRDTs), - case ?DICT:size(Remaining) of - 0 -> %% Ctx remove removed all dots for field + SurvivingDots = riak_dt_vclock:subtract_dots(Dots, Ctx), + case SurvivingDots of + [] -> %% Ctx remove removed all dots for field empty; _ -> %% Update the tombstone with the GLB clock - {Remaining, Type:merge(TS, TS0)} + CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), + %% Always reset to empty clock so we don't duplicate storage + {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2)} end; ctx_rem_field(Field, Values, Ctx, MapClock) -> - ctx_rem_field(Field, ?DICT:find(Field, Values), Ctx, MapClock). + ctx_rem_field(Field, dict:find(Field, Values), Ctx, MapClock). %% @private If we're asked to remove something we don't have (or have, %% but maybe not all 'updates' for it), is it because we've not seen @@ -436,132 +427,46 @@ defer_remove(Clock, Ctx, Field, Deferred) -> case riak_dt_vclock:descends(Clock, Ctx) of %% no need to save this remove, we're done true -> Deferred; - false -> ?DICT:update(Ctx, + false -> dict:update(Ctx, fun(Fields) -> ordsets:add_element(Field, Fields) end, ordsets:add_element(Field, ordsets:new()), Deferred) end. -%% @doc merge two `map()'s. --spec merge(map(), map()) -> map(). -merge(Map, Map) -> - Map; -%% @TODO is there a way to optimise this, based on clocks maybe? merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) -> Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), - {CommonKeys, LHSUnique, RHSUnique} = key_sets(LHSEntries, RHSEntries), - Acc0 = filter_unique(LHSUnique, LHSEntries, RHSClock, ?DICT:new()), - Acc1 = filter_unique(RHSUnique, RHSEntries, LHSClock, Acc0), - Entries = merge_common(CommonKeys, LHSEntries, RHSEntries, LHSClock, RHSClock, Acc1), - Deferred = merge_deferred(RHSDeferred, LHSDeferred), + Fields = lists:umerge(dict:fetch_keys(LHSEntries), dict:fetch_keys(RHSEntries)), + Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> + {LHSDots, LHSCRDT} = get_entry(Field, LHSEntries, LHSClock), + {RHSDots, RHSCRDT} = get_entry(Field, RHSEntries, RHSClock), + case keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) of + [] -> + Acc; + Dots -> + CRDT = Type:merge(LHSCRDT, RHSCRDT), + %% Yes! Reset the clock, again + dict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT)}, Acc) + end + end, + dict:new(), + Fields), + Deferred = merge_deferred(LHSDeferred, RHSDeferred), apply_deferred(Clock, Entries, Deferred). -%% @private filter the set of fields that are on one side of a merge -%% only. --spec filter_unique(set(), entries(), riak_dt_vclock:vclock(), entries()) -> entries(). -filter_unique(FieldSet, Entries, Clock, Acc) -> - sets:fold(fun({_Name, Type}=Field, Keep) -> - {Dots, TS} = ?DICT:fetch(Field, Entries), - KeepDots = ?DICT:filter(fun(Dot, _CRDT) -> - is_dot_unseen(Dot, Clock) - end, - Dots), - - case ?DICT:size(KeepDots) of - 0 -> - Keep; - _ -> - %% create a tombstone since the - %% otherside does not have this field, - %% it either removed it, or never had - %% it. If it never had it, the removing - %% dots in the tombstone will have no - %% impact on the value, if the otherside - %% removed it, then the removed dots - %% will be propogated by the tombstone. - Tombstone = Type:merge(TS, Type:parent_clock(Clock, Type:new())), - ?DICT:store(Field, {KeepDots, Tombstone}, Keep) - end - end, - Acc, - FieldSet). - -%% @private predicate function, `true' if the provided `dot()' is -%% concurrent with the clock, `false' if the clock has seen the dot. --spec is_dot_unseen(riak_dt:dot(), riak_dt_vclock:vclock()) -> boolean(). -is_dot_unseen(Dot, Clock) -> - not riak_dt_vclock:descends(Clock, [Dot]). - -%% @doc Get the keys from an ?DICT as a set --spec key_set(?DICT()) -> set(). -key_set(Dict) -> - sets:from_list(?DICT:fetch_keys(Dict)). - -%% @doc break the keys from an two ?DICTs out into three sets, the -%% common keys, those unique to one, and those unique to the other. --spec key_sets(?DICT(), ?DICT()) -> {set(), set(), set()}. -key_sets(LHS, RHS) -> - LHSet = key_set(LHS), - RHSet = key_set(RHS), - {sets:intersection(LHSet, RHSet), - sets:subtract(LHSet, RHSet), - sets:subtract(RHSet, LHSet)}. - - -%% @private for a set of dots (that are unique to one side) decide -%% whether to keep, or drop each. --spec filter_dots(set(), ?DICT(), riak_dt_vclock:vclock()) -> entries(). -filter_dots(Dots, CRDTs, Clock) -> - DotsToKeep = sets:filter(fun(Dot) -> - is_dot_unseen(Dot, Clock) - end, - Dots), - - ?DICT:filter(fun(Dot, _CRDT) -> - sets:is_element(Dot, DotsToKeep) - end, - CRDTs). - -%% @private merge the common fields into a set of surviving dots and a -%% tombstone per field. If a dot is on both sides, keep it. If it is -%% only on one side, drop it if dominated by the otheride's clock. -merge_common(FieldSet, LHS, RHS, LHSClock , RHSClock, Acc) -> - sets:fold(fun({_, Type}=Field, Keep) -> - {LHSDots, LHTS} = ?DICT:fetch(Field, LHS), - {RHSDots, RHTS} = ?DICT:fetch(Field, RHS), - {CommonDots, LHSUniqe, RHSUnique} = key_sets(LHSDots, RHSDots), - TS = Type:merge(RHTS, LHTS), - - CommonSurviving = sets:fold(fun(Dot, Common) -> - L = ?DICT:fetch(Dot, LHSDots), - ?DICT:store(Dot, L, Common) - end, - ?DICT:new(), - CommonDots), - - LHSSurviving = filter_dots(LHSUniqe, LHSDots, RHSClock), - RHSSurviving = filter_dots(RHSUnique, RHSDots, LHSClock), - - Dots = ?DICT:from_list(lists:merge([?DICT:to_list(CommonSurviving), - ?DICT:to_list(LHSSurviving), - ?DICT:to_list(RHSSurviving)])), - - case ?DICT:size(Dots) of - 0 -> - Keep; - _ -> - ?DICT:store(Field, {Dots, TS}, Keep) - end - - end, - Acc, - FieldSet). +keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> + CommonDots = sets:intersection(sets:from_list(LHSDots), sets:from_list(RHSDots)), + LHSUnique = sets:to_list(sets:subtract(sets:from_list(LHSDots), CommonDots)), + RHSUnique = sets:to_list(sets:subtract(sets:from_list(RHSDots), CommonDots)), + LHSKeep = riak_dt_vclock:subtract_dots(LHSUnique, RHSClock), + RHSKeep = riak_dt_vclock:subtract_dots(RHSUnique, LHSClock), + riak_dt_vclock:merge([sets:to_list(CommonDots), LHSKeep, RHSKeep]). + %% @private -spec merge_deferred(deferred(), deferred()) -> deferred(). merge_deferred(LHS, RHS) -> - ?DICT:merge(fun(_K, LH, RH) -> + dict:merge(fun(_K, LH, RH) -> ordsets:union(LH, RH) end, LHS, RHS). @@ -597,19 +502,11 @@ equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> pairwise_equals(lists:sort(?DICT:to_list(Values1)), lists:sort(?DICT:to_list(Values2))). --spec pairwise_equals([field()], [field()]) -> boolean(). +-spec pairwise_equals(entries(), entries()) -> boolean(). pairwise_equals([], []) -> true; -pairwise_equals([{{Name, Type}, {Dots1, TS1}}| Rest1], [{{Name, Type}, {Dots2, TS2}}|Rest2]) -> - %% Tombstones don't need to be equal. When we merge with a map - %% where one side is absent, we take the absent sides clock, when - %% we merge where both sides have a field, we merge the - %% tombstones, and apply deferred. The deferred remove uses a glb - %% of the context and the clock, meaning we get a smaller - %% tombstone. Both are correct when it comes to determining the - %% final value. As long as tombstones are not conflicting (that is - %% A == B | A > B | B > A) - case {?DICT:fetch_keys(Dots1) == ?DICT:fetch_keys(Dots2), Type:equal(TS1, TS2)} of +pairwise_equals([{{Name, Type}, {Dots1, CRDT1}}|Rest1], [{{Name, Type}, {Dots2, CRDT2}}|Rest2]) -> + case {riak_dt_vclock:equal(Dots1, Dots2), Type:equal(CRDT1, CRDT2)} of {true, true} -> pairwise_equals(Rest1, Rest2); _ -> @@ -636,25 +533,18 @@ precondition_context({Clock, _Field, _Deferred}) -> %% of lag/staleness. -spec stats(map()) -> [{atom(), integer()}]. stats(Map) -> - [ {S, stat(S, Map)} || S <- [actor_count, field_count, duplication, deferred_length]]. + [ {S, stat(S, Map)} || S <- [actor_count, field_count, deferred_length]]. -spec stat(atom(), map()) -> number() | undefined. stat(actor_count, {Clock, _, _}) -> length(Clock); stat(field_count, {_, Fields, _}) -> ?DICT:size(Fields); -stat(duplication, {_, Fields, _}) -> - %% Number of duplicated fields - {FieldCnt, Duplicates} = ?DICT:fold(fun(_Field, {Dots ,_}, {FCnt, DCnt}) -> - {FCnt+1, DCnt + ?DICT:size(Dots)} - end, - {0, 0}, - Fields), - Duplicates - FieldCnt; stat(deferred_length, {_, _, Deferred}) -> ?DICT:size(Deferred); stat(_,_) -> undefined. + -include("riak_dt_tags.hrl"). -define(TAG, ?DT_MAP_TAG). -define(V1_VERS, 1). @@ -736,6 +626,7 @@ from_binary(?V2_VERS, <>) -> Map= riak_dt:from_binary(B), to_v2(Map). + %% =================================================================== %% EUnit tests %% =================================================================== @@ -886,18 +777,14 @@ stat_test() -> {ok, Map2} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"foo">>, 3}}]}, a2, Map1), {ok, Map3} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"bar">>, 4}}]}, a3, Map1), Map4 = merge(Map2, Map3), - ?assertEqual([{actor_count, 0}, {field_count, 0}, {duplication, 0}, {deferred_length, 0}], stats(Map)), + ?assertEqual([{actor_count, 0}, {field_count, 0}, {deferred_length, 0}], stats(Map)), ?assertEqual(3, stat(actor_count, Map4)), ?assertEqual(5, stat(field_count, Map4)), ?assertEqual(undefined, stat(waste_pct, Map4)), - ?assertEqual(1, stat(duplication, Map4)), {ok, Map5} = update({update, [{update, {l3, riak_dt_lwwreg}, {assign, <<"baz">>, 5}}]}, a3, Map4), ?assertEqual(6, stat(field_count, Map5)), - ?assertEqual(1, stat(duplication, Map5)), - %% Updating field {l, riak_dt_lwwreg} merges the duplicates to a single field %% @see apply_ops {ok, Map6} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"bim">>, 6}}]}, a2, Map5), - ?assertEqual(0, stat(duplication, Map6)), {ok, Map7} = update({update, [{remove, {l, riak_dt_lwwreg}}]}, a1, Map6), ?assertEqual(5, stat(field_count, Map7)). @@ -910,24 +797,6 @@ equals_test() -> ?assert(equal(C, D)), ?assert(equal(A, A)). -%% up/downgrade tests -v1_v2_test() -> - {ok, Map} = update({update, [{update, {<<"set">>, riak_dt_orswot}, {add_all, [<<"bar">>, <<"baz">>]}}]}, a, new()), - V2Bin = to_binary(?V2_VERS, Map), - V1Bin = to_binary(?V1_VERS, Map), - Map2 = from_binary(?V2_VERS, V1Bin), - V1Map = from_binary(?V1_VERS, V1Bin), - V1MapFromV2 = from_binary(?V1_VERS, V2Bin), - ?assertMatch(<>, V1Bin), - ?assertMatch(<>, V2Bin), - ?assert(equal(Map, Map2)), - ?assertEqual(V1MapFromV2, V1Map), - ?assertEqual(V2Bin, to_binary(Map)), - ?assertEqual(V1Bin, to_binary(?V1_VERS, V1Map)), - ?assert(equal(Map, from_binary(V2Bin))), - ?assert(equal(Map, from_binary(V1Bin))), - ?assert(equal(Map, from_binary(?V2_VERS, V2Bin))). - -ifdef(EQC). -define(NUMTESTS, 1000). @@ -974,9 +843,9 @@ gen_field(Size) -> , elements([ riak_dt_emcntr, riak_dt_orswot, - riak_dt_lwwreg, +%% riak_dt_lwwreg, riak_dt_od_flag - ] ++ [riak_dt_map || Size > 0])}. + ] ++ [?MODULE || Size > 0])}. gen_field_op({_Name, Type}, Size) -> Type:gen_op(Size). diff --git a/src/riak_dt_map2.erl b/src/riak_dt_map2.erl deleted file mode 100644 index 837c636..0000000 --- a/src/riak_dt_map2.erl +++ /dev/null @@ -1,807 +0,0 @@ -%% ------------------------------------------------------------------- -%% -%% riak_dt_map2: OR-Set schema based multi CRDT container -%% -%% Copyright (c) 2007-2013 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 multi CRDT holder. A Struct/Document-ish thing. Uses the -%% same tombstone-less, Observed Remove semantics as `riak_dt_orswot'. -%% A Map is set of `Field's a `Field' is a two-tuple of: -%% `{Name::binary(), CRDTModule::module()}' where the second element -%% is the name of a crdt module that may be embedded. CRDTs stored -%% inside the Map will have their `update/3/4' function called, but the -%% second argument will be a `riak_dt:dot()', so that they share the -%% causal context of the map, even when fields are removed, and -%% subsequently re-added. -%% -%% The contents of the Map are modeled as a dictionary of -%% `field_name()' to `field_value()' mappings. Where `field_ name()' -%% is a two tuple of an opaque `binary()' name, and one of the -%% embeddable crdt types (currently `riak_dt_orswot', -%% `riak_dt_emcntr', `riak_dt_lwwreg', `riak_dt_od_flag', and -%% `riak_dt_map'). The reason for this limitation is that embedded -%% types must support embedding: that is a shared, `dot'-based, causal -%% context, and a reset-remove semantic (more on these below.) The -%% `field_value()' is a two-tuple of `entries()' and a -%% `tombstone()'. The presence of a `tombstone()' in a "tombstoneless" -%% Map is confusing. The `tombstone()' is only stored for fields that -%% are currently in the map, removing a field also removes its -%% tombstone. -%% -%% To use the Map create a `new()' Map. When you call `update/3' or -%% `update/4' you pass a list of operations and an optional causal -%% context. @See `update/3' or `update/4' for more details. The list -%% of operations is applied atomically in full, and new state -%% returned, or not at all, and an error is returned. -%% -%%

Semantics

-%% -%% The semantics of this Map are Observed-Remove-Reset-Remove. What -%% this means is practice is, if a field is removed, and concurrently -%% that same field is updated, the field is _in_ the Map (only -%% observed updates are removed) but those removes propagate, so only -%% the concurrent update survives. A concrete example helps: If a Map -%% contains a field that is a set, and the set has 5 elements in it, -%% and concurrently the replica at A removes the field that contains -%% the set, while the replica at B adds an item to the set, on merge -%% there is a field for the set, but it contains only the one item B -%% added. The removal of the field is semantically equivalent to -%% removing all elements in the set, and removing the field. The same -%% goes for an embedded Map. If concurrently a Map field is removed, -%% while a new sub-field is updated, only the updated field(s) survive -%% the reset-remove. -%% -%% There is an anomaly for embedded counters that does not fully -%% support reset remove. Embedded counters (@see riak_dt_emcntr) are -%% different to a normal `pn-counter'. Embedded counters map `dot's to -%% {P, N} pairs. When a counter is incremented a new dot is created, -%% that replaces the old dot with the new value. `pn-counter' usually -%% merges by taking the `max' of any `P' or `N' entry for an -%% actor. This does not work in an embedded context. When a counter -%% field is removed, and then _re_-added, the new `P' and `N' entries -%% may be lower than the old, and merging loses the remove -%% information. However, if a `dot' is stored with the value, and the -%% max of the `dot' is used in merge, new updates win over removed -%% updates. So far so good. Here is the problem. If Replica B removes -%% a counter field, and does not re-add it, and replica A concurrently -%% updates it's entry for that field, then the reset-remove does not -%% occur. All new dots are not `observed' by Replica B, so not -%% removed. The new `dots' contain the updates from the previous -%% `dots', and the old `dot' is discarded. To achieve reset-remove all -%% increments would need a dot, and need to be retained, which would -%% be very costly in terms of space. One way to accept this anomaly is -%% to think of a Map like a file system: removing a directory and -%% concurrently adding a file means that the directory is present and -%% only the file remains in it. Updating a counter and concurrently -%% removing it, means the counter remains, with the updated value, -%% much like appending to a file in the file system analogy: you don't -%% expect only the diff to survive, but the whole updated file. -%% -%%

Merging/Size

-%% -%% When any pair of Maps are merged, the embedded CRDTs are _not_ -%% merged, instead each concurrent `dot'->`field()' entry is -%% kept. This leads to a greater size for Maps that are highly -%% divergent. Updating a field in the map, however, leads to all -%% entries for that field being merged to a single CRDT that is stored -%% against the new `dot'. As mentioned above, there is also a -%% `tombstone' entry per present field. This is bottom CRDT for the -%% field type with a clock that contains all seen and removed -%% `dots'. There tombstones are merged at merge time, so only one is -%% present per field. Clearly the repetition of actor information (the -%% clock, each embedded CRDT, the field `dots', the tombstones) is a -%% serious issue with regard to size/bloat of this data type. We use -%% erlang's `to_binary/2' function, which compresses the data, to get -%% around this at present. -%% -%%

Context and Deferred operations

-%% -%% For CRDTs that use version vectors and dots (this `Map' and all -%% CRDTs that may be embedded in it), the size of the CRDT is -%% influenced by the number of actors updating it. In some systems -%% (like Riak!) we attempt to minimize the number of actors by only -%% having the database update CRDTs. This leads to a kind of "action -%% at a distance", where a client sends operations to the database, -%% and an actor in the database system performs the operations. The -%% purpose is to ship minimal state between database and client, and -%% to limit the number of actors in the system. There is a problem -%% with action at a distance and the OR semantic. The client _must_ be -%% able to tell the database what has been observed when it sends a -%% remove operation. There is a further problem. A replica that -%% handles an operation may not have all the state the client -%% observed. We solve these two problems by asking the client to -%% provide a causal context for operations (@see `update/4'.) Context -%% operations solve the OR problem, but they don't solve the problem -%% of lagging replicas handling operations. -%% -%%

Lagging replicas, deferred operations

-%% -%% In a system like Riak, a replica that is not up-to-date (including, -%% never seen any state for a CRDT) maybe asked to perform an -%% operation. If no context is given, and the operation is a field -%% remove, or a "remove" like operation on an embedded CRDT, the -%% operation may fail with a precondition error (for example, remove a -%% field that is not present) or succeed and remove more state than -%% intended (a field remove with no context may remove updates unseen -%% by the client.) When a context is provided, and the Field to be -%% removed is absent, the Map state stores the context, and Field -%% name, in a list of deferred operations. When, eventually, through -%% propagation and merging, the Map's clock descends the context for -%% the operation, the operation is executed. It is important to note -%% that _only_ actorless (field remove) operations can occur this way. -%% -%%

Embedded CRDTs Deferred Operations

-%% -%% There is a bug with embedded types and deferred operations. Imagine -%% a client has seen a Map with a Set field, and the set contains {a, -%% b, c}. The client sends an operation to remove {a} from the set. A -%% replica that is new takes the operation. It will create a new Map, -%% a Field for the Set, and store the `remove` operation as part of -%% the Set's state. A client reads this new state, and sends a field -%% remove operation, that is executed by same replica. Now the -%% deferred operation is lost, since the field is removed. We're -%% working on ways to fix this. One idea is to not remove a field with -%% "undelivered" operations, but instead to "hide" it. -%% -%% @see riak_dt_orswot for more on the OR semantic -%% @see riak_dt_emcntr for the embedded counter. -%% @end - --module(riak_dt_map2). - --behaviour(riak_dt). - --ifdef(EQC). --include_lib("eqc/include/eqc.hrl"). --endif. - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - -%% API --export([new/0, value/1, value/2, update/3, update/4]). --export([merge/2, equal/2, to_binary/1, from_binary/1]). --export([precondition_context/1, stats/1, stat/2]). --export([parent_clock/2]). - -%% EQC API --ifdef(EQC). --export([gen_op/0, gen_op/1, gen_field/0, gen_field/1, generate/0, size/1]). --endif. - --export_type([map/0, binary_map/0, map_op/0]). - --type binary_map() :: binary(). %% A binary that from_binary/1 will accept --type map() :: {riak_dt_vclock:vclock(), entries(), deferred()}. --type entries() :: [field()]. --type field() :: {field_name(), field_value()}. --type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. --type field_value() :: {riak_dt_vclock:vclock(), crdt()}. - -%%-type crdts() :: [entry()]. -%%-type entry() :: {riak_dt:dot(), crdt()}. - -%% Only for present fields, ensures removes propogate -%%-type tombstone() :: crdt(). - -%% Only field removals can be deferred. CRDTs stored in the map may -%% have contexts and deferred operations, but as these are part of the -%% state, they are stored under the field as an update like any other. --type deferred() :: [{context(), [field()]}]. - -%% limited to only those mods that support both a shared causal -%% context, and by extension, the reset-remove semantic. --type crdt_mod() :: riak_dt_emcntr | riak_dt_lwwreg | - riak_dt_od_flag | - riak_dt_map | riak_dt_orswot. - --type crdt() :: riak_dt_emcntr:emcntr() | riak_dt_od_flag:od_flag() | - riak_dt_lwwreg:lwwreg() | - riak_dt_orswot:orswot() | - riak_dt_map:map(). - --type map_op() :: {update, [map_field_update() | map_field_op()]}. - --type map_field_op() :: {remove, field()}. --type map_field_update() :: {update, field(), crdt_op()}. - --type crdt_op() :: riak_dt_emcntr:emcntr_op() | - riak_dt_lwwreg:lwwreg_op() | - riak_dt_orswot:orswot_op() | riak_dt_od_flag:od_flag_op() | - riak_dt_map:map_op(). - --type context() :: riak_dt_vclock:vclock() | undefined. - --type values() :: [value()]. --type value() :: {field(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. --type precondition_error() :: {error, {precondition, {not_present, field()}}}. - --define(FRESH_CLOCK, riak_dt_vclock:fresh()). - -%% @doc Create a new, empty Map. --spec new() -> map(). -new() -> - {riak_dt_vclock:fresh(), orddict:new(), orddict:new()}. - -%% @doc sets the clock in the map to that `Clock'. Used by a -%% containing Map for sub-CRDTs --spec parent_clock(riak_dt_vclock:vclock(), map()) -> map(). -parent_clock(Clock, {_MapClock, Values, Deferred}) -> - {Clock, Values, Deferred}. - -%% @doc get the current set of values for this Map --spec value(map()) -> values(). -value({_Clock, Values, _Deferred}) -> - orddict:fold(fun({Name, Type}, {_Dots, CRDT}, Acc) -> - [{{Name, Type}, Type:value(CRDT)} | Acc] end, - [], - Values). - -%% @doc query map (not implemented yet) --spec value(term(), map()) -> values(). -value(_, Map) -> - value(Map). - -%% @doc update the `map()' or a field in the `map()' by executing -%% the `map_op()'. `Ops' is a list of one or more of the following -%% ops: -%% -%% `{update, field(), Op} where `Op' is a valid update operation for a -%% CRDT of type `Mod' from the `Key' pair `{Name, Mod}' If there is no -%% local value for `Key' a new CRDT is created, the operation applied -%% and the result inserted otherwise, the operation is applied to the -%% local value. -%% -%% `{remove, `field()'}' where field is `{name, type}', results in -%% the crdt at `field' and the key and value being removed. A -%% concurrent `update' will "win" over a remove so that the field is -%% still present, and it's value will contain the concurrent update. -%% -%% Atomic, all of `Ops' are performed successfully, or none are. --spec update(map_op(), riak_dt:actor() | riak_dt:dot(), map()) -> - {ok, map()} | precondition_error(). -update(Op, ActorOrDot, Map) -> - update(Op, ActorOrDot, Map, undefined). - -%% @doc the same as `update/3' except that the context ensures no -%% unseen field updates are removed, and removal of unseen updates is -%% deferred. The Context is passed down as the context for any nested -%% types. hence the common clock. -%% -%% @see parent_clock/2 --spec update(map_op(), riak_dt:actor() | riak_dt:dot(), map(), riak_dt:context()) -> - {ok, map()}. -update({update, Ops}, ActorOrDot, {Clock0, Values, Deferred}, Ctx) -> - {Dot, Clock} = update_clock(ActorOrDot, Clock0), - apply_ops(Ops, Dot, {Clock, Values, Deferred}, Ctx). - -%% @private update the clock, and get a dot for the operations. This -%% means that field removals increment the clock too. --spec update_clock(riak_dt:actor() | riak_dt:dot(), - riak_dt_vclock:vclock()) -> - {riak_dt:dot(), riak_dt_vclock:vclock()}. -update_clock(Dot, Clock) when is_tuple(Dot) -> - NewClock = riak_dt_vclock:merge([[Dot], Clock]), - {Dot, NewClock}; -update_clock(Actor, Clock) -> - NewClock = riak_dt_vclock:increment(Actor, Clock), - Dot = {Actor, riak_dt_vclock:get_counter(Actor, NewClock)}, - {Dot, NewClock}. - -get({_Name, Type}=Field, Fields, Clock) -> - CRDT = case orddict:find(Field, Fields) of - {ok, {_Dots, CRDT0}} -> - CRDT0; - error -> - Type:new() - end, - Type:parent_clock(Clock, CRDT). - -get_entry({_Name, Type}=Field, Fields, Clock) -> - {Dots, CRDT} = case orddict:find(Field, Fields) of - {ok, Entry} -> - Entry; - error -> - {[], Type:new()} - end, - {Dots, Type:parent_clock(Clock, CRDT)}. - -%% @private --spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), - {riak_dt_vclock:vclock(), entries() , deferred()}, context()) -> - {ok, map()} | precondition_error(). -apply_ops([], _Dot, Map, _Ctx) -> - {ok, Map}; -apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> - CRDT = get(Field, Values, Clock), - case Type:update(Op, Dot, CRDT, Ctx) of - {ok, Updated0} -> - Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), - NewValues = orddict:store(Field, {[Dot], Updated}, Values), - apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); - Error -> - Error - end; -apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> - case remove_field(Field, Map, Ctx) of - {ok, NewMap} -> - apply_ops(Rest, Dot, NewMap, Ctx); - E -> - E - end. - -%% @private when context is undefined, we simply remove all instances -%% of Field, regardless of their dot. If the field is not present then -%% we warn the user with a precondition error. However, in the case -%% that a context is provided we can be more fine grained, and only -%% remove those field entries whose dots are seen by the context. This -%% preserves the "observed" part of "observed-remove". There is no -%% precondition error if we're asked to remove smoething that isn't -%% present, either we defer it, or it has been done already, depending -%% on if the Map clock descends the context clock or not. -%% -%% @see defer_remove/4 for handling of removes of fields that are -%% _not_ present --spec remove_field(field(), map(), context()) -> - {ok, map()} | precondition_error(). -remove_field(Field, {Clock, Values, Deferred}, undefined) -> - case orddict:find(Field, Values) of - error -> - {error, {precondition, {not_present, Field}}}; - {ok, _Removed} -> - {ok, {Clock, orddict:erase(Field, Values), Deferred}} - end; -%% Context removes -remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> - Deferred = defer_remove(Clock, Ctx, Field, Deferred0), - NewValues = case ctx_rem_field(Field, Values, Ctx, Clock) of - empty -> - orddict:erase(Field, Values); - CRDT -> - orddict:store(Field, CRDT, Values) - end, - {ok, {Clock, NewValues, Deferred}}. - -%% @private drop dominated fields -ctx_rem_field(_Field, error, _Ctx_, _Clock) -> - empty; -ctx_rem_field({_, Type}, {ok, {Dots, CRDT}}, Ctx, MapClock) -> - %% Drop dominated fields, and update the tombstone. - %% - %% If the context is removing a field at dot {a, 1} and the - %% current field is {a, 2}, the tombstone ensures that all events - %% from {a, 1} are removed from the crdt value. If the ctx remove - %% is at {a, 3} and the current field is at {a, 2} then we need to - %% remove only events upto {a, 2}. The glb clock enables that. - %% - TombstoneClock = riak_dt_vclock:glb(Ctx, MapClock), %% GLB is events seen by both clocks only - TS = Type:parent_clock(TombstoneClock, Type:new()), - SurvivingDots = riak_dt_vclock:subtract_dots(Dots, Ctx), - case SurvivingDots of - [] -> %% Ctx remove removed all dots for field - empty; - _ -> - %% Update the tombstone with the GLB clock - CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), - %% Always reset to empty clock so we don't duplicate storage - {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2)} - end; -ctx_rem_field(Field, Values, Ctx, MapClock) -> - ctx_rem_field(Field, orddict:find(Field, Values), Ctx, MapClock). - -%% @private If we're asked to remove something we don't have (or have, -%% but maybe not all 'updates' for it), is it because we've not seen -%% the some update that we've been asked to remove, or is it because -%% we already removed it? In the former case, we can "defer" this -%% operation by storing it, with its context, for later execution. If -%% the clock for the Map descends the operation clock, then we don't -%% need to defer the op, its already been done. It is _very_ important -%% to note, that only _actorless_ operations can be saved. That is -%% operations that DO NOT need to increment the clock. In a Map this -%% means field removals only. Contexts for update operations do not -%% result in deferred operations on the parent Map. This simulates -%% causal delivery, in that an `update' must be seen before it can be -%% `removed'. --spec defer_remove(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), field(), deferred()) -> - deferred(). -defer_remove(Clock, Ctx, Field, Deferred) -> - case riak_dt_vclock:descends(Clock, Ctx) of - %% no need to save this remove, we're done - true -> Deferred; - false -> orddict:update(Ctx, - fun(Fields) -> - ordsets:add_element(Field, Fields) end, - ordsets:add_element(Field, ordsets:new()), - Deferred) - end. - -merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) -> - Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), - Fields = lists:umerge(orddict:fetch_keys(LHSEntries), orddict:fetch_keys(RHSEntries)), - Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> - {LHSDots, LHSCRDT} = get_entry(Field, LHSEntries, LHSClock), - {RHSDots, RHSCRDT} = get_entry(Field, RHSEntries, RHSClock), - case keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) of - [] -> - Acc; - Dots -> - CRDT = Type:merge(LHSCRDT, RHSCRDT), - %% Yes! Reset the clock, again - orddict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT)}, Acc) - end - end, - orddict:new(), - Fields), - Deferred = merge_deferred(LHSDeferred, RHSDeferred), - apply_deferred(Clock, Entries, Deferred). - -keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> - CommonDots = sets:intersection(sets:from_list(LHSDots), sets:from_list(RHSDots)), - LHSUnique = sets:to_list(sets:subtract(sets:from_list(LHSDots), CommonDots)), - RHSUnique = sets:to_list(sets:subtract(sets:from_list(RHSDots), CommonDots)), - LHSKeep = riak_dt_vclock:subtract_dots(LHSUnique, RHSClock), - RHSKeep = riak_dt_vclock:subtract_dots(RHSUnique, LHSClock), - riak_dt_vclock:merge([sets:to_list(CommonDots), LHSKeep, RHSKeep]). - - -%% @private --spec merge_deferred(deferred(), deferred()) -> deferred(). -merge_deferred(LHS, RHS) -> - orddict:merge(fun(_K, LH, RH) -> - ordsets:union(LH, RH) end, - LHS, RHS). - -%% @private apply those deferred field removals, if they're -%% preconditions have been met, that is. --spec apply_deferred(riak_dt_vclock:vclock(), entries(), deferred()) -> - {riak_dt_vclock:vclock(), entries(), deferred()}. -apply_deferred(Clock, Entries, Deferred) -> - lists:foldl(fun({Ctx, Fields}, Map) -> - remove_all(Fields, Map, Ctx) - end, - {Clock, Entries, []}, - Deferred). - -%% @private --spec remove_all([field()], map(), context()) -> - map(). -remove_all(Fields, Map, Ctx) -> - lists:foldl(fun(Field, MapAcc) -> - {ok, MapAcc2}= remove_field(Field, MapAcc, Ctx), - MapAcc2 - end, - Map, - Fields). - -%% @doc compare two `map()'s for equality of structure Both schemas -%% and value list must be equal. Performs a pariwise equals for all -%% values in the value lists --spec equal(map(), map()) -> boolean(). -equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> - riak_dt_vclock:equal(Clock1, Clock2) andalso - Deferred1 == Deferred2 andalso - pairwise_equals(Values1, Values2). - --spec pairwise_equals(entries(), entries()) -> boolean(). -pairwise_equals([], []) -> - true; -pairwise_equals([{{Name, Type}, {Dots1, CRDT1}}|Rest1], [{{Name, Type}, {Dots2, CRDT2}}|Rest2]) -> - case {riak_dt_vclock:equal(Dots1, Dots2), Type:equal(CRDT1, CRDT2)} of - {true, true} -> - pairwise_equals(Rest1, Rest2); - _ -> - false - end; -pairwise_equals(_, _) -> - false. - -%% @doc an opaque context that can be passed to `update/4' to ensure -%% that only seen fields are removed. If a field removal operation has -%% a context that the Map has not seen, it will be deferred until -%% causally relevant. --spec precondition_context(map()) -> riak_dt:context(). -precondition_context({Clock, _Field, _Deferred}) -> - Clock. - -%% @doc stats on internal state of Map. -%% A proplist of `{StatName :: atom(), Value :: integer()}'. Stats exposed are: -%% `actor_count': The number of actors in the clock for the Map. -%% `field_count': The total number of fields in the Map (including divergent field entries). -%% `duplication': The number of duplicate entries in the Map across all fields. -%% basically `field_count' - ( unique fields) -%% `deferred_length': How many operations on the deferred list, a reasonable expression -%% of lag/staleness. --spec stats(map()) -> [{atom(), integer()}]. -stats(Map) -> - [ {S, stat(S, Map)} || S <- [actor_count, field_count, duplication, deferred_length]]. - --spec stat(atom(), map()) -> number() | undefined. -stat(actor_count, {Clock, _, _}) -> - length(Clock); -stat(field_count, {_, Fields, _}) -> - length(Fields); -stat(duplication, {_, Fields, _}) -> - %% Number of duplicated fields - {FieldCnt, Duplicates} = orddict:fold(fun(_Field, {Dots ,_}, {FCnt, DCnt}) -> - {FCnt+1, DCnt + orddict:size(Dots)} - end, - {0, 0}, - Fields), - Duplicates - FieldCnt; -stat(deferred_length, {_, _, Deferred}) -> - length(Deferred); -stat(_,_) -> undefined. - --include("riak_dt_tags.hrl"). --define(TAG, ?DT_MAP_TAG). --define(V1_VERS, 1). - -%% @doc returns a binary representation of the provided `map()'. The -%% resulting binary is tagged and versioned for ease of future -%% upgrade. Calling `from_binary/1' with the result of this function -%% will return the original map. Use the application env var -%% `binary_compression' to turn t2b compression on (`true') and off -%% (`false') -%% -%% @see `from_binary/1' --spec to_binary(map()) -> binary_map(). -to_binary(Map) -> - <>. - -%% @doc When the argument is a `binary_map()' produced by -%% `to_binary/1' will return the original `map()'. -%% -%% @see `to_binary/1' --spec from_binary(binary_map()) -> map(). -from_binary(<>) -> - riak_dt:from_binary(B). - - -%% =================================================================== -%% EUnit tests -%% =================================================================== --ifdef(TEST). - -%% This fails on previous version of riak_dt_map -assoc_test() -> - Field = {'X', riak_dt_orswot}, - {ok, A} = update({update, [{update, Field, {add, 0}}]}, a, new()), - {ok, B} = update({update, [{update, Field, {add, 0}}]}, b, new()), - {ok, B2} = update({update, [{update, Field, {remove, 0}}]}, b, B), - C = A, - {ok, C3} = update({update, [{remove, Field}]}, c, C), - ?assertEqual(merge(A, merge(B2, C3)), merge(merge(A, B2), C3)), - ?assertEqual(value(merge(merge(A, C3), B2)), value(merge(merge(A, B2), C3))), - ?assertEqual(merge(merge(A, C3), B2), merge(merge(A, B2), C3)). - -clock_test() -> - Field = {'X', riak_dt_orswot}, - {ok, A} = update({update, [{update, Field, {add, 0}}]}, a, new()), - B = A, - {ok, B2} = update({update, [{update, Field, {add, 1}}]}, b, B), - {ok, A2} = update({update, [{update, Field, {remove, 0}}]}, a, A), - {ok, A3} = update({update, [{remove, Field}]}, a, A2), - {ok, A4} = update({update, [{update, Field, {add, 2}}]}, a, A3), - AB = merge(A4, B2), - ?assertEqual([{Field, [1, 2]}], value(AB)). - -remfield_test() -> - Field = {'X', riak_dt_orswot}, - {ok, A} = update({update, [{update, Field, {add, 0}}]}, a, new()), - B = A, - {ok, A2} = update({update, [{update, Field, {remove, 0}}]}, a, A), - {ok, A3} = update({update, [{remove, Field}]}, a, A2), - {ok, A4} = update({update, [{update, Field, {add, 2}}]}, a, A3), - AB = merge(A4, B), - ?assertEqual([{Field, [2]}], value(AB)). - -%% Bug found by EQC, not dropping dots in merge when an element is -%% present in both Maos leads to removed items remaining after merge. -present_but_removed_test() -> - F = {'X', riak_dt_lwwreg}, - %% Add Z to A - {ok, A} = update({update, [{update, F, {assign, <<"A">>}}]}, a, new()), - %% Replicate it to C so A has 'Z'->{a, 1} - C = A, - %% Remove Z from A - {ok, A2} = update({update, [{remove, F}]}, a, A), - %% Add Z to B, a new replica - {ok, B} = update({update, [{update, F, {assign, <<"B">>}}]}, b, new()), - %% Replicate B to A, so now A has a Z, the one with a Dot of - %% {b,1} and clock of [{a, 1}, {b, 1}] - A3 = merge(B, A2), - %% Remove the 'Z' from B replica - {ok, B2} = update({update, [{remove, F}]}, b, B), - %% Both C and A have a 'Z', but when they merge, there should be - %% no 'Z' as C's has been removed by A and A's has been removed by - %% C. - Merged = lists:foldl(fun(Set, Acc) -> - merge(Set, Acc) end, - %% the order matters, the two replicas that - %% have 'Z' need to merge first to provoke - %% the bug. You end up with 'Z' with two - %% dots, when really it should be removed. - A3, - [C, B2]), - ?assertEqual([], value(Merged)). - - -%% A bug EQC found where dropping the dots in merge was not enough if -%% you then store the value with an empty clock (derp). -no_dots_left_test() -> - F = {'Z', riak_dt_lwwreg}, - {ok, A} = update({update, [{update, F, {assign, <<"A">>}}]}, a, new()), - {ok, B} = update({update, [{update, F, {assign, <<"B">>}}]}, b, new()), - C = A, %% replicate A to empty C - {ok, A2} = update({update, [{remove, F}]}, a, A), - %% replicate B to A, now A has B's 'Z' - A3 = merge(A2, B), - %% Remove B's 'Z' - {ok, B2} = update({update, [{remove, F}]}, b, B), - %% Replicate C to B, now B has A's old 'Z' - B3 = merge(B2, C), - %% Merge everytyhing, without the fix You end up with 'Z' present, - %% with no dots - Merged = lists:foldl(fun(Set, Acc) -> - merge(Set, Acc) end, - A3, - [B3, C]), - ?assertEqual([], value(Merged)). - -%% A reset-remove bug eqc found where dropping a superseded dot lost -%% field remove merge information the dropped dot contained, adding -%% the tombstone fixed this. -tombstone_remove_test() -> - F = {'X', riak_dt_orswot}, - A=B=new(), - {ok, A1} = update({update, [{update, F, {add, 0}}]}, a, A), - %% Replicate! - B1 = merge(A1, B), - {ok, A2} = update({update, [{remove, F}]}, a, A1), - {ok, B2} = update({update, [{update, F, {add, 1}}]}, b, B1), - %% Replicate - A3 = merge(A2, B2), - %% that remove of F from A means remove the 0 A added to F - ?assertEqual([{F, [1]}], value(A3)), - {ok, B3} = update({update, [{update, F, {add, 2}}]}, b, B2), - %% replicate to A - A4 = merge(A3, B3), - %% final values - Final = merge(A4, B3), - %% before adding the tombstone, the dropped dots were simply - %% merged with the surviving field. When the second update to B - %% was merged with A, that information contained in the superseded - %% field in A at {b,1} was lost (since it was merged into the - %% _VALUE_). This casued the [0] from A's first dot to - %% resurface. By adding the tombstone, the superseded field merges - %% it's tombstone with the surviving {b, 2} field so the remove - %% information is preserved, even though the {b, 1} value is - %% dropped. Pro-tip, don't alter the CRDTs' values in the merge! - ?assertEqual([{F, [1,2]}], value(Final)). - -%% This test is a regression test for a counter example found by eqc. -%% The previous version of riak_dt_map used the `dot' from the field -%% update/creation event as key in `merge_left/3'. Of course multiple -%% fields can be added/updated at the same time. This means they get -%% the same `dot'. When merging two replicas, it is possible that one -%% has removed one or more of the fields added at a particular `dot', -%% which meant a function clause error in `merge_left/3'. The -%% structure was wrong, it didn't take into account the possibility -%% that multiple fields could have the same `dot', when clearly, they -%% can. This test fails with `dot' as the key for a field in -%% `merge_left/3', but passes with the current structure, of -%% `{field(), dot()}' as key. -dot_key_test() -> - {ok, A} = update({update, [{update, {'X', riak_dt_orswot}, {add, <<"a">>}}, {update, {'X', riak_dt_od_flag}, enable}]}, a, new()), - B = A, - {ok, A2} = update({update, [{remove, {'X', riak_dt_od_flag}}]}, a, A), - ?assertEqual([{{'X', riak_dt_orswot}, [<<"a">>]}], value(merge(B, A2))). - -stat_test() -> - Map = new(), - {ok, Map1} = update({update, [{update, {c, riak_dt_emcntr}, increment}, - {update, {s, riak_dt_orswot}, {add, <<"A">>}}, - {update, {m, riak_dt_map}, {update, [{update, {ss, riak_dt_orswot}, {add, 0}}]}}, - {update, {l, riak_dt_lwwreg}, {assign, <<"a">>, 1}}, - {update, {l2, riak_dt_lwwreg}, {assign, <<"b">>, 2}}]}, a1, Map), - {ok, Map2} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"foo">>, 3}}]}, a2, Map1), - {ok, Map3} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"bar">>, 4}}]}, a3, Map1), - Map4 = merge(Map2, Map3), - ?assertEqual([{actor_count, 0}, {field_count, 0}, {duplication, 0}, {deferred_length, 0}], stats(Map)), - ?assertEqual(3, stat(actor_count, Map4)), - ?assertEqual(5, stat(field_count, Map4)), - ?assertEqual(undefined, stat(waste_pct, Map4)), - ?assertEqual(1, stat(duplication, Map4)), - {ok, Map5} = update({update, [{update, {l3, riak_dt_lwwreg}, {assign, <<"baz">>, 5}}]}, a3, Map4), - ?assertEqual(6, stat(field_count, Map5)), - ?assertEqual(1, stat(duplication, Map5)), - %% Updating field {l, riak_dt_lwwreg} merges the duplicates to a single field - %% @see apply_ops - {ok, Map6} = update({update, [{update, {l, riak_dt_lwwreg}, {assign, <<"bim">>, 6}}]}, a2, Map5), - ?assertEqual(0, stat(duplication, Map6)), - {ok, Map7} = update({update, [{remove, {l, riak_dt_lwwreg}}]}, a1, Map6), - ?assertEqual(5, stat(field_count, Map7)). - -equals_test() -> - {ok, A} = update({update, [{update, {'X', riak_dt_orswot}, {add, <<"a">>}}, {update, {'X', riak_dt_od_flag}, enable}]}, a, new()), - {ok, B} = update({update, [{update, {'Y', riak_dt_orswot}, {add, <<"a">>}}, {update, {'Z', riak_dt_od_flag}, enable}]}, b, new()), - ?assert(not equal(A, B)), - C = merge(A, B), - D = merge(B, A), - ?assert(equal(C, D)), - ?assert(equal(A, A)). - --ifdef(EQC). --define(NUMTESTS, 1000). - -%% =================================== -%% crdt_statem_eqc callbacks -%% =================================== -size(Map) -> - %% How big is a Map? Maybe number of fields and depth matter? But - %% then the number of fields in sub maps too? - byte_size(term_to_binary(Map)) div 10. - -generate() -> - ?LET({Ops, Actors}, {non_empty(list(gen_op())), non_empty(list(bitstring(16*8)))}, - lists:foldl(fun(Op, Map) -> - Actor = case length(Actors) of - 1 -> hd(Actors); - _ -> lists:nth(crypto:rand_uniform(1, length(Actors)), Actors) - end, - case update(Op, Actor, Map) of - {ok, M} -> M; - _ -> Map - end - end, - new(), - Ops)). - -%% Add depth parameter -gen_op() -> - ?SIZED(Size, gen_op(Size)). - -gen_op(Size) -> - ?LET(Ops, non_empty(list(gen_update(Size))), {update, Ops}). - -gen_update(Size) -> - ?LET(Field, gen_field(Size), - oneof([{remove, Field}, - {update, Field, gen_field_op(Field, Size div 2)}])). - -gen_field() -> - ?SIZED(Size, gen_field(Size)). - -gen_field(Size) -> - {growingelements(['A', 'B', 'C', 'X', 'Y', 'Z']) %% Macro? Bigger? - , elements([ - riak_dt_emcntr, - riak_dt_orswot, -%% riak_dt_lwwreg, - riak_dt_od_flag - ] ++ [?MODULE || Size > 0])}. - -gen_field_op({_Name, Type}, Size) -> - Type:gen_op(Size). - - --endif. - --endif. diff --git a/test/map2_eqc.erl b/test/map2_eqc.erl deleted file mode 100644 index 07a4043..0000000 --- a/test/map2_eqc.erl +++ /dev/null @@ -1,685 +0,0 @@ -%% ------------------------------------------------------------------- -%% -%% map_eqc: Drive out the merge bugs the other statem couldn't -%% -%% Copyright (c) 2007-2014 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(map_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,{replicas=[], - %% a unique tag per add - counter=1 :: pos_integer(), - %% fields that have been added - adds=[] :: [{atom(), module()}] - }). - --define(NUMTESTS, 1000). --define(QC_OUT(P), - eqc:on_output(fun(Str, Args) -> - io:format(user, Str, Args) end, P)). -%% @doc eunit runner -eqc_test_() -> - {timeout, 200, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(100, ?QC_OUT(prop_merge()))))}. - -%% @doc eunit runner for bin roundtrip -bin_roundtrip_test_() -> - {timeout, 100, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(50, ?QC_OUT(crdt_statem_eqc:prop_bin_roundtrip(riak_dt_map)))))}. - -%% @doc shell convenience, run eqc property 1000 times. -run() -> - run(?NUMTESTS). - -%% @doc shell convenience, run eqc property `Count' times -run(Count) -> - eqc:quickcheck(eqc:numtests(Count, prop_merge())). - -%% @doc shell convenience, check eqc property (last failing counter example) -check() -> - eqc:check(prop_merge()). - -%% Initialize the state --spec initial_state() -> eqc_statem:symbolic_state(). -initial_state() -> - #state{}. - -%% ------ Grouped operator: create_replica -create_replica_pre(#state{replicas=Replicas}) -> - length(Replicas) < 10. - -%% @doc create_replica_arge - Generate a replica -create_replica_args(_S) -> - %% don't waste time shrinking actor id binaries - [noshrink(binary(8))]. - -%% @doc create_replica_pre - Don't duplicate replicas --spec create_replica_pre(S :: eqc_statem:symbolic_state(), - Args :: [term()]) -> boolean(). -create_replica_pre(#state{replicas=Replicas}, [Id]) -> - not lists:member(Id, Replicas). - -%% @doc store new replica ID and bottom map/model in ets -create_replica(Id) -> - ets:insert(map_eqc, {Id, riak_dt_map:new(), model_new()}). - -%% @doc create_replica_next - Add replica ID to state --spec create_replica_next(S :: eqc_statem:symbolic_state(), - V :: eqc_statem:var(), - Args :: [term()]) -> eqc_statem:symbolic_state(). -create_replica_next(S=#state{replicas=R0}, _Value, [Id]) -> - S#state{replicas=R0++[Id]}. - - -%% ------ Grouped operator: remove -%% @doc remove, but only something that has been added already -remove_pre(#state{replicas=Replicas, adds=Adds}) -> - Replicas /= [] andalso Adds /= []. - -%% @doc remove something that has been added already from any replica -remove_args(#state{adds=Adds, replicas=Replicas}) -> - [ - elements(Replicas), - elements(Adds) %% A Field that has been added - ]. - -%% @doc correct shrinking -remove_pre(#state{replicas=Replicas}, [Replica, _]) -> - lists:member(Replica, Replicas). - -%% @doc perform a remove operation, remove `Field' from Map/Model at -%% `Replica' -remove(Replica, Field) -> - [{Replica, Map, Model}] = ets:lookup(map_eqc, Replica), - %% even though we only remove what has been added, there is no - %% guarantee a merge from another replica hasn't led to the - %% Field being removed already, so ignore precon errors (they - %% don't change state) - {ok, Map2} = ignore_precon_error(riak_dt_map:update({update, [{remove, Field}]}, Replica, Map), Map), - Model2 = model_remove_field(Field, Model), - ets:insert(map_eqc, {Replica, Map2, Model2}), - {Map2, Model2}. - -%% @doc remove post condition, @see post_all/2 -remove_post(_S, [_Replica, Field], Res) -> - post_all(Res, remove) andalso field_not_present(Field, Res). - -%% ------ Grouped operator: ctx_remove -%% @doc remove, but with a context -ctx_remove_pre(#state{replicas=Replicas, adds=Adds}) -> - Replicas /= [] andalso Adds /= []. - -%% @doc generate ctx rmeove args -ctx_remove_args(#state{replicas=Replicas, adds=Adds}) -> - [ - elements(Replicas), %% read from - elements(Replicas), %% send op to - elements(Adds) %% which field to remove - ]. - -%% @doc ensure correct shrinking -ctx_remove_pre(#state{replicas=Replicas, adds=Adds}, [From, To, Field]) -> - lists:member(From, Replicas) andalso lists:member(To, Replicas) - andalso lists:member(Field, Adds). - -%% @doc dynamic precondition, only context remove if the `Field' is in -%% the `From' replicas -ctx_remove_dynamicpre(_S, [From, _To, Field]) -> - [{From, Map, _Model}] = ets:lookup(map_eqc, From), - lists:keymember(Field, 1, riak_dt_map:value(Map)). - -%% @doc perform a context remove on the map and model using context -%% from `From' -ctx_remove(From, To, Field) -> - [{From, FromMap, FromModel}] = ets:lookup(map_eqc, From), - [{To, ToMap, ToModel}] = ets:lookup(map_eqc, To), - Ctx = riak_dt_map:precondition_context(FromMap), - {ok, Map} = riak_dt_map:update({update, [{remove, Field}]}, To, ToMap, Ctx), - Model = model_ctx_remove(Field, FromModel, ToModel), - ets:insert(map_eqc, {To, Map, Model}), - {Map, Model}. - -%% @doc @see post_all/2 -ctx_remove_post(_S, _Args, Res) -> - post_all(Res, ctx_remove). - -%% ------ Grouped operator: replicate Merges two replicas' values. - -%% @doc must be a replica at least. -replicate_pre(#state{replicas=Replicas}) -> - Replicas /= []. - -%% @doc chose from/to replicas, can be the same -replicate_args(#state{replicas=Replicas}) -> - [ - elements(Replicas), %% Replicate from - elements(Replicas) %% Replicate to - ]. - -%% @doc replicate_pre - shrink correctly --spec replicate_pre(S :: eqc_statem:symbolic_state(), - Args :: [term()]) -> boolean(). -replicate_pre(#state{replicas=Replicas}, [From, To]) -> - lists:member(From, Replicas) andalso lists:member(To, Replicas). - -%% @doc Replicate a CRDT from `From' to `To' -replicate(From, To) -> - [{From, FromMap, FromModel}] = ets:lookup(map_eqc, From), - [{To, ToMap, ToModel}] = ets:lookup(map_eqc, To), - - Map = riak_dt_map:merge(FromMap, ToMap), - Model = model_merge(FromModel, ToModel), - - ets:insert(map_eqc, {To, Map, Model}), - {Map, Model}. - -%% @doc @see post_all/2 -replicate_post(_S, _Args, Res) -> - post_all(Res, rep). - -%% ------ Grouped operator: ctx_update -%% Update a Field in the Map, using the Map context - -%% @doc there must be at least one replica -ctx_update_pre(#state{replicas=Replicas}) -> - Replicas /= []. - -%% @doc generate an operation -ctx_update_args(#state{replicas=Replicas, counter=Cnt}) -> - ?LET({Field, Op}, gen_field_and_op(), - [ - Field, - Op, - elements(Replicas), - elements(Replicas), - Cnt - ]). - -%% @doc ensure correct shrinking -ctx_update_pre(#state{replicas=Replicas}, [_Field, _Op, From, To, _Cnt]) -> - lists:member(From, Replicas) andalso lists:member(To, Replicas). - -%% @doc much like context_remove, get a contet from `From' and apply -%% `Op' at `To' -ctx_update(Field, Op, From, To, Cnt) -> - [{From, CtxMap, CtxModel}] = ets:lookup(map_eqc, From), - [{To, ToMap, ToModel}] = ets:lookup(map_eqc, To), - - Ctx = riak_dt_map:precondition_context(CtxMap), - ModCtx = model_ctx(CtxModel), - {ok, Map} = riak_dt_map:update({update, [{update, Field, Op}]}, To, ToMap, Ctx), - {ok, Model} = model_update_field(Field, Op, To, Cnt, ToModel, ModCtx), - - ets:insert(map_eqc, {To, Map, Model}), - {Map, Model}. - -%% @doc update the model state, incrementing the counter represents logical time. -ctx_update_next(S=#state{counter=Cnt, adds=Adds}, _Res, [Field, _Op, _From, _To, _Cnt]) -> - S#state{adds=lists:umerge(Adds, [Field]), counter=Cnt+1}. - -%% @doc @see post_all/2 -ctx_update_post(_S, _Args, Res) -> - post_all(Res, update). - -%% ------ Grouped operator: update -%% Update a Field in the Map - -%% @doc there must be at least one replica -update_pre(#state{replicas=Replicas}) -> - Replicas /= []. - -%% @doc choose a field, operation and replica -update_args(#state{replicas=Replicas, counter=Cnt}) -> - ?LET({Field, Op}, gen_field_and_op(), - [ - Field, - Op, - elements(Replicas), - Cnt - ]). - -%% @doc shrink correctly -update_pre(#state{replicas=Replicas}, [_Field, _Op, Replica, _Cnt]) -> - lists:member(Replica, Replicas). - -%% @doc apply `Op' to `Field' at `Replica' -update(Field, Op, Replica, Cnt) -> - [{Replica, Map0, Model0}] = ets:lookup(map_eqc, Replica), - - {ok, Map} = ignore_precon_error(riak_dt_map:update({update, [{update, Field, Op}]}, Replica, Map0), Map0), - {ok, Model} = model_update_field(Field, Op, Replica, Cnt, Model0, undefined), - - ets:insert(map_eqc, {Replica, Map, Model}), - {Map, Model}. - -%% @doc increment the time counter, and add this field to adds as a -%% candidate to be removed later. -update_next(S=#state{counter=Cnt, adds=Adds}, _Res, [Field, _, _, _]) -> - S#state{adds=lists:umerge(Adds, [Field]), counter=Cnt+1}. - -%% @doc @see post_all/2 -update_post(_S, _Args, Res) -> - post_all(Res, update). - -%% ------ Grouped operator: idempotent - -idempotent_args(#state{replicas=Replicas}) -> - [elements(Replicas)]. - -%% @doc idempotent_pre - Precondition for generation --spec idempotent_pre(S :: eqc_statem:symbolic_state()) -> boolean(). -idempotent_pre(#state{replicas=Replicas}) -> - Replicas /= []. - -%% @doc idempotent_pre - Precondition for idempotent --spec idempotent_pre(S :: eqc_statem:symbolic_state(), - Args :: [term()]) -> boolean(). -idempotent_pre(#state{replicas=Replicas}, [Replica]) -> - lists:member(Replica, Replicas). - -%% @doc idempotent - Merge replica with itself, result used for post condition only -idempotent(Replica) -> - [{Replica, Map, _Model}] = ets:lookup(map_eqc, Replica), - {Map, riak_dt_map:merge(Map, Map)}. - -%% @doc idempotent_post - Postcondition for idempotent --spec idempotent_post(S :: eqc_statem:dynamic_state(), - Args :: [term()], R :: term()) -> true | term(). -idempotent_post(_S, [_Replica], {Map, MergedSelfMap}) -> - riak_dt_map:equal(Map, MergedSelfMap). - -%% ------ Grouped operator: commutative - -commutative_args(#state{replicas=Replicas}) -> - [elements(Replicas), elements(Replicas)]. - -%% @doc commutative_pre - Precondition for generation --spec commutative_pre(S :: eqc_statem:symbolic_state()) -> boolean(). -commutative_pre(#state{replicas=Replicas}) -> - Replicas /= []. - -%% @doc commutative_pre - Precondition for commutative --spec commutative_pre(S :: eqc_statem:symbolic_state(), - Args :: [term()]) -> boolean(). -commutative_pre(#state{replicas=Replicas}, [Replica, Replica2]) -> - lists:member(Replica, Replicas) andalso lists:member(Replica2, Replicas). - -%% @doc commutative - Merge maps both ways (result used for post condition) -commutative(Replica1, Replica2) -> - [{Replica1, Map1, _Model1}] = ets:lookup(map_eqc, Replica1), - [{Replica2, Map2, _Model2}] = ets:lookup(map_eqc, Replica2), - {riak_dt_map:merge(Map1, Map2), riak_dt_map:merge(Map2, Map1)}. - -%% @doc commutative_post - Postcondition for commutative --spec commutative_post(S :: eqc_statem:dynamic_state(), - Args :: [term()], R :: term()) -> true | term(). -commutative_post(_S, [_Replica1, _Replica2], {OneMergeTwo, TwoMergeOne}) -> - riak_dt_map:equal(OneMergeTwo, TwoMergeOne). - -%% ------ Grouped operator: associative - -associative_args(#state{replicas=Replicas}) -> - [elements(Replicas), elements(Replicas), elements(Replicas)]. - -%% @doc associative_pre - Precondition for generation --spec associative_pre(S :: eqc_statem:symbolic_state()) -> boolean(). -associative_pre(#state{replicas=Replicas}) -> - Replicas /= []. - -%% @doc associative_pre - Precondition for associative --spec associative_pre(S :: eqc_statem:symbolic_state(), - Args :: [term()]) -> boolean(). -associative_pre(#state{replicas=Replicas}, [Replica, Replica2, Replica3]) -> - lists:member(Replica, Replicas) - andalso lists:member(Replica2, Replicas) - andalso lists:member(Replica3, Replicas). - -%% @doc associative - Merge maps three ways (result used for post condition) -associative(Replica1, Replica2, Replica3) -> - [{Replica1, Map1, _Model1}] = ets:lookup(map_eqc, Replica1), - [{Replica2, Map2, _Model2}] = ets:lookup(map_eqc, Replica2), - [{Replica3, Map3, _Model3}] = ets:lookup(map_eqc, Replica3), - {riak_dt_map:merge(riak_dt_map:merge(Map1, Map2), Map3), - riak_dt_map:merge(riak_dt_map:merge(Map1, Map3), Map2), - riak_dt_map:merge(riak_dt_map:merge(Map2, Map3), Map1)}. - -%% @doc associative_post - Postcondition for associative --spec associative_post(S :: eqc_statem:dynamic_state(), - Args :: [term()], R :: term()) -> true | term(). -associative_post(_S, [_Replica1, _Replica2, _Replica3], {ABC, ACB, BCA}) -> -%% case {riak_dt_map:equal(ABC, ACB), riak_dt_map:equal(ACB, BCA)} of - case {map_values_equal(ABC, ACB), map_values_equal(ACB, BCA)} of - {true, true} -> - true; - {false, true} -> - {postcondition_failed, {ACB, not_associative, ABC}}; - {true, false} -> - {postcondition_failed, {ACB, not_associative, BCA}}; - {false, false} -> - {postcondition_failed, {{ACB, not_associative, ABC}, '&&', {ACB, not_associative, BCA}}} - end. - -map_values_equal(Map1, Map2) -> - lists:sort(riak_dt_map:value(Map1)) == lists:sort(riak_dt_map:value(Map2)). - -%% @Doc Weights for commands. Don't create too many replicas, but -%% prejudice in favour of creating more than 1. Try and balance -%% removes with adds. But favour adds so we have something to -%% remove. See the aggregation output. -weight(S, create_replica) when length(S#state.replicas) > 2 -> - 1; -weight(S, create_replica) when length(S#state.replicas) < 5 -> - 4; -weight(_S, remove) -> - 2; -weight(_S, context_remove) -> - 5; -weight(_S, update) -> - 3; -weight(_S, ctx_update) -> - 3; -weight(_S, _) -> - 1. - - -%% @doc Tests the property that a riak_dt_map is equivalent to the Map -%% Model. The Map Model is based roughly on an or-set design. Inspired -%% by a draft spec in sent in private email by Carlos Baquero. The -%% model extends the spec to onclude context operations, deferred -%% operations, and reset-remove semantic (with tombstones.) -prop_merge() -> - ?FORALL(Cmds, commands(?MODULE), - begin - %% store state in eqc, external to statem state - ets:new(map_eqc, [named_table, set]), - {H, S, Res} = run_commands(?MODULE,Cmds), - ReplicaData = ets:tab2list(map_eqc), - %% Check that merging all values leads to the same results for Map and the Model - {Map, Model} = lists:foldl(fun({_Actor, InMap, InModel}, {M, Mo}) -> - {riak_dt_map:merge(M, InMap), - model_merge(Mo, InModel)} - end, - {riak_dt_map:new(), model_new()}, - ReplicaData), - MapValue = riak_dt_map:value(Map), - ModelValue = model_value(Model), - %% clean up - ets:delete(map_eqc), - %% prop - pretty_commands(?MODULE, Cmds, {H, S, Res}, - measure(actors, length(ReplicaData), - measure(length, length(MapValue), - measure(depth, map_depth(MapValue), - aggregate(command_names(Cmds), - conjunction([{results, equals(Res, ok)}, - {value, equals(lists:sort(MapValue), lists:sort(ModelValue))} - ]) - ))))) - end). - -%% ----------- -%% Generators -%% ---------- - -%% @doc Keep the number of possible field names down to a minimum. The -%% smaller state space makes EQC more likely to find bugs since there -%% will be more action on the fields. Learned this from -%% crdt_statem_eqc having to large a state space and missing bugs. -gen_field() -> - {growingelements(['A', 'B', 'C', 'D', 'X', 'Y', 'Z']), - elements([ - riak_dt_orswot, - riak_dt_emcntr, - riak_dt_lwwreg, - riak_dt_map, - riak_dt_od_flag - ])}. - -%% @use the generated field to generate an op. Delegates to type. Some -%% Type generators are recursive, pass in Size to limit the depth of -%% recusrions. @see riak_dt_map:gen_op/1. -gen_field_op({_Name, Type}) -> - ?SIZED(Size, Type:gen_op(Size)). - -%% @doc geneate a field, and a valid operation for the field -gen_field_and_op() -> - ?LET(Field, gen_field(), {Field, gen_field_op(Field)}). - - -%% ----------- -%% Helpers -%% ---------- - -%% @doc how deeply nested is `Map'? Recurse down the Map and return -%% the deepest nesting. A depth of `1' means only a top-level Map. `2' -%% means a map in the seocnd level, `3' in the third, and so on. -map_depth(Map) -> - map_depth(Map, 1). - -%% @doc iterate a maps fields, and recurse down map fields to get a -%% max depth. -map_depth(Map, D) -> - lists:foldl(fun({{_, riak_dt_map}, SubMap}, MaxDepth) -> - Depth = map_depth(SubMap, D+1), - max(MaxDepth, Depth); - (_, Depth) -> - Depth - end, - D, - Map). - -%% @doc precondition errors don't change the state of a map, so ignore -%% them. -ignore_precon_error({ok, NewMap}, _) -> - {ok, NewMap}; -ignore_precon_error(_, Map) -> - {ok, Map}. - -%% @doc for all mutating operations enusre that the state at the -%% mutated replica is equal for the map and the model -post_all({Map, Model}, Cmd) -> - %% What matters is that both types have the exact same results. - case lists:sort(riak_dt_map:value(Map)) == lists:sort(model_value(Model)) of - true -> - true; - _ -> - {postcondition_failed, "Map and Model don't match", Cmd, Map, Model, riak_dt_map:value(Map), model_value(Model)} - end. - -%% @doc `true' if `Field' is not in `Map' -field_not_present(Field, {Map, _Model}) -> - case lists:keymember(Field, 1, riak_dt_map:value(Map)) of - false -> - true; - true -> - {Field, present, Map} - end. - -%% ----------- -%% Model -%% ---------- - -%% The model has to be a Map CRDT, with the same deferred operations -%% and reset-remove semantics as riak_dt_map. Luckily it has no -%% efficiency constraints. It's modelled as three lists. Added fields, -%% removed fields, and deferred remove operations. There is also an -%% entry for tombstones, and vclock. Whenever a field is removed, an -%% empty CRDT of that fields type is stored, with the clock at removal -%% time, in tombstones. Merging a fields value with this tombstone -%% ensures reset-remove semantics. The vector clock entry is only for -%% tombstones and context operations. The add/remove sets use a unique -%% tag, like in OR-Set, for elements. - --record(model, { - %% Things added to the Map, a Set really, but uses a list - %% for ease of reading in case of a counter example - adds=[], - %% Tombstones of things removed from the map - removes=[], - %% Removes that are waiting for adds before they - %% can be run (like context ops in the - %% riak_dt_map) - deferred=[], - %% For reset-remove semantic, the field+clock at time of - %% removal - tombstones=orddict:new() :: orddict:orddict(), - %% for embedded context operations - clock=riak_dt_vclock:fresh() :: riak_dt_vclock:vclock() - }). - --type map_model() :: #model{}. - -%% @doc create a new model. This is the bottom element. --spec model_new() -> map_model(). -model_new() -> - #model{}. - -%% @doc update `Field' with `Op', by `Actor' at time `Cnt'. very -%% similar to riak_dt_map. --spec model_update_field({atom(), module()}, term(), binary(), pos_integer(), - map_model(), undefined | riak_dt_vclock:vclock()) -> map_model(). -model_update_field({_Name, Type}=Field, Op, Actor, Cnt, Model, Ctx) -> - #model{adds=Adds, removes=Removes, clock=Clock, tombstones=TS} = Model, - %% generate a new clock - Clock2 = riak_dt_vclock:merge([[{Actor, Cnt}], Clock]), - %% Get those fields that are present, that is, only in the Adds - %% set - InMap = lists:subtract(Adds, Removes), - %% Merge all present values to get a current CRDT, and add each - %% field entry we merge to a set so we can move them to `removed' - {CRDT0, ToRem} = lists:foldl(fun({F, Value, _X}=E, {CAcc, RAcc}) when F == Field -> - {Type:merge(CAcc, Value), lists:umerge([E], RAcc)}; - (_, Acc) -> Acc - end, - {Type:new(), []}, - InMap), - %% If we have a tombstone for this field, merge with it to ensure - %% reset-remove - CRDT1 = case orddict:find(Field, TS) of - error -> - CRDT0; - {ok, TSVal} -> - Type:merge(CRDT0, TSVal) - end, - %% Update the clock to ensure a shared causal context - CRDT = Type:parent_clock(Clock2, CRDT1), - %% Apply the operation - case Type:update(Op, {Actor, Cnt}, CRDT, Ctx) of - {ok, Updated} -> - %% Op succeded, store the new value as the field, using - %% `Cnt' (which is time since the statem acts serially) as - %% a unique tag. - Model2 = Model#model{adds=lists:umerge([{Field, Updated, Cnt}], Adds), - removes=lists:umerge(ToRem, Removes), - clock=Clock2}, - {ok, Model2}; - _ -> - %% if the op failed just return the state un changed - {ok, Model} - end. - -%% @doc remove a field from the model, doesn't enforce a precondition, -%% removing a non-present field does nothing. --spec model_remove_field({atom(), module()}, map_model()) -> map_model(). -model_remove_field({_Name, Type}=Field, Model) -> - #model{adds=Adds, removes=Removes, tombstones=Tombstones0, clock=Clock} = Model, - ToRemove = [{F, Val, Token} || {F, Val, Token} <- Adds, F == Field], - TS = Type:parent_clock(Clock, Type:new()), - Tombstones = orddict:update(Field, fun(T) -> Type:merge(TS, T) end, TS, Tombstones0), - Model#model{removes=lists:umerge(Removes, ToRemove), tombstones=Tombstones}. - -%% @doc merge two models. Very simple since the model truley always -%% grows, it is just a union of states, and then applies the deferred -%% operations. --spec model_merge(map_model(), map_model()) -> map_model(). -model_merge(Model1, Model2) -> - #model{adds=Adds1, removes=Removes1, deferred=Deferred1, clock=Clock1, tombstones=TS1} = Model1, - #model{adds=Adds2, removes=Removes2, deferred=Deferred2, clock=Clock2, tombstones=TS2} = Model2, - Clock = riak_dt_vclock:merge([Clock1, Clock2]), - Adds0 = lists:umerge(Adds1, Adds2), - Tombstones = orddict:merge(fun({_Name, Type}, V1, V2) -> Type:merge(V1, V2) end, TS1, TS2), - Removes0 = lists:umerge(Removes1, Removes2), - Deferred0 = lists:umerge(Deferred1, Deferred2), - {Adds, Removes, Deferred} = model_apply_deferred(Adds0, Removes0, Deferred0), - #model{adds=Adds, removes=Removes, deferred=Deferred, clock=Clock, tombstones=Tombstones}. - -%% @doc apply deferred operations that may be relevant now a merge as taken place. --spec model_apply_deferred(list(), list(), list()) -> {list(), list(), list()}. -model_apply_deferred(Adds, Removes, Deferred) -> - D2 = lists:subtract(Deferred, Adds), - ToRem = lists:subtract(Deferred, D2), - {Adds, lists:umerge(ToRem, Removes), D2}. - -%% @doc remove `Field' from `To' using `From's context. --spec model_ctx_remove({atom(), module()}, map_model(), map_model()) -> map_model(). -model_ctx_remove({_N, Type}=Field, From, To) -> - %% get adds for Field, any adds for field in ToAdds that are in - %% FromAdds should be removed any others, put in deferred - #model{adds=FromAdds, clock=FromClock} = From, - #model{adds=ToAdds, removes=ToRemoves, deferred=ToDeferred, tombstones=TS} = To, - ToRemove = lists:filter(fun({F, _Val, _Token}) -> F == Field end, FromAdds), - Defer = lists:subtract(ToRemove, ToAdds), - Remove = lists:subtract(ToRemove, Defer), - Tombstone = Type:parent_clock(FromClock, Type:new()), - TS2 = orddict:update(Field, fun(T) -> Type:merge(T, Tombstone) end, Tombstone, TS), - To#model{removes=lists:umerge(Remove, ToRemoves), - deferred=lists:umerge(Defer, ToDeferred), - tombstones=TS2}. - -%% @doc get the actual value for a model --spec model_value(map_model()) -> - [{Field::{atom(), module()}, Value::term()}]. -model_value(Model) -> - #model{adds=Adds, removes=Removes, tombstones=TS} = Model, - Remaining = lists:subtract(Adds, Removes), - %% fold over fields that are in the map and merge each to a merge - %% CRDT per field - Res = lists:foldl(fun({{_Name, Type}=Key, Value, _X}, Acc) -> - %% if key is in Acc merge with it and replace - dict:update(Key, fun(V) -> - Type:merge(V, Value) end, - Value, Acc) end, - dict:new(), - Remaining), - %% fold over merged CRDTs and merge with the tombstone (if any) - %% for each field - Res2 = dict:fold(fun({_N, Type}=Field, Val, Acc) -> - case orddict:find(Field, TS) of - error -> - dict:store(Field, Val, Acc); - {ok, TSVal} -> - dict:store(Field, Type:merge(TSVal, Val), Acc) - end - end, - dict:new(), - Res), - %% Call `value/1' on each CRDT in the model - [{K, Type:value(V)} || {{_Name, Type}=K, V} <- dict:to_list(Res2)]. - -%% @doc get a context for a model context operation --spec model_ctx(map_model()) -> riak_dt_vclock:vclock(). -model_ctx(#model{clock=Ctx}) -> - Ctx. - - --endif. % EQC From 3278a6d3f4f44a3265b7c704702891606a787c09 Mon Sep 17 00:00:00 2001 From: balegas Date: Mon, 29 Dec 2014 19:04:10 +0000 Subject: [PATCH 03/33] unit tests --- src/riak_dt_map.erl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index bf157b6..ac43b9c 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -632,6 +632,33 @@ from_binary(?V2_VERS, <>) -> %% =================================================================== -ifdef(TEST). +keep_deferred_test() -> + Field = {'X', riak_dt_od_flag}, + InitialState = riak_dt_map:new(), + + %Update at node A + {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, Field, enable}]}, a, InitialState), + %Update with the context of node A on node B generates a deferred. + {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, Field, disable}]}, b, InitialState, CtxA1), + %Remove field in B is causal with the disable op - it removes the field and the deferred. + {ok, StateB2} = update({update, [{remove, Field}]}, b, StateB1, CtxB1), + StateAB = merge(StateA1,StateB2), + ?assertEqual([],value(StateAB)). + +keep_deferred_with_concurrent_add_test() -> + Field = {'X', riak_dt_od_flag}, + InitialState = riak_dt_map:new(), + + %Update at node A + {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, Field, enable}]}, a, InitialState), + %Update with the context of node A on node B generates a deferred. + {ok, {_, _, _}=StateA2} = update({update, [{update, Field, enable}]}, a, StateA1, CtxA1), + {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, Field, disable}]}, b, InitialState, CtxA1), + %Remove field in B is causal with the disable op - it removes the field and the deferred. + {ok, StateB2} = update({update, [{remove, Field}]}, b, StateB1, CtxB1), + StateAB = merge(StateA2,StateB2), + ?assertEqual([{{'X',riak_dt_od_flag},true}],value(StateAB)). + %% This fails on previous version of riak_dt_map assoc_test() -> Field = {'X', riak_dt_orswot}, From 92a49b16a25a5c08cbd031a98f107148ffb62af1 Mon Sep 17 00:00:00 2001 From: balegas Date: Fri, 2 Jan 2015 16:52:12 +0000 Subject: [PATCH 04/33] Work in progress: remove leaf --- src/riak_dt_map.erl | 103 +++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index ac43b9c..b70cbdc 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -194,7 +194,7 @@ -type entries() :: [field()]. -type field() :: {field_name(), field_value()}. -type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. --type field_value() :: {riak_dt_vclock:vclock(), crdt()}. +-type field_value() :: {riak_dt_vclock:vclock(), crdt(), riak_dt_vclock:vclock()}. %%-type crdts() :: [entry()]. %%-type entry() :: {riak_dt:dot(), crdt()}. @@ -210,13 +210,13 @@ %% limited to only those mods that support both a shared causal %% context, and by extension, the reset-remove semantic. -type crdt_mod() :: riak_dt_emcntr | riak_dt_lwwreg | - riak_dt_od_flag | - riak_dt_map | riak_dt_orswot. +riak_dt_od_flag | +riak_dt_map | riak_dt_orswot. -type crdt() :: riak_dt_emcntr:emcntr() | riak_dt_od_flag:od_flag() | - riak_dt_lwwreg:lwwreg() | - riak_dt_orswot:orswot() | - riak_dt_map:map(). +riak_dt_lwwreg:lwwreg() | +riak_dt_orswot:orswot() | +riak_dt_map:map(). -type map_op() :: {update, [map_field_update() | map_field_op()]}. @@ -224,9 +224,9 @@ -type map_field_update() :: {update, field(), crdt_op()}. -type crdt_op() :: riak_dt_emcntr:emcntr_op() | - riak_dt_lwwreg:lwwreg_op() | - riak_dt_orswot:orswot_op() | riak_dt_od_flag:od_flag_op() | - riak_dt_map:map_op(). +riak_dt_lwwreg:lwwreg_op() | +riak_dt_orswot:orswot_op() | riak_dt_od_flag:od_flag_op() | +riak_dt_map:map_op(). -type context() :: riak_dt_vclock:vclock() | undefined. @@ -251,7 +251,7 @@ parent_clock(Clock, {_MapClock, Values, Deferred}) -> %% @doc get the current set of values for this Map -spec value(map()) -> values(). value({_Clock, Values, _Deferred}) -> - lists:sort(dict:fold(fun({Name, Type}, {_Dots, CRDT}, Acc) -> + lists:sort(dict:fold(fun({Name, Type}, {_Dots, CRDT, _ContextT}, Acc) -> [{{Name, Type}, Type:value(CRDT)} | Acc] end, [], Values)). @@ -278,7 +278,7 @@ value(_, Map) -> %% %% Atomic, all of `Ops' are performed successfully, or none are. -spec update(map_op(), riak_dt:actor() | riak_dt:dot(), map()) -> - {ok, map()} | precondition_error(). + {ok, map()} | precondition_error(). update(Op, ActorOrDot, Map) -> update(Op, ActorOrDot, Map, undefined). @@ -289,7 +289,7 @@ update(Op, ActorOrDot, Map) -> %% %% @see parent_clock/2 -spec update(map_op(), riak_dt:actor() | riak_dt:dot(), map(), riak_dt:context()) -> - {ok, map()}. + {ok, map()}. update({update, Ops}, ActorOrDot, {Clock0, Values, Deferred}, Ctx) -> {Dot, Clock} = update_clock(ActorOrDot, Clock0), apply_ops(Ops, Dot, {Clock, Values, Deferred}, Ctx). @@ -298,7 +298,7 @@ update({update, Ops}, ActorOrDot, {Clock0, Values, Deferred}, Ctx) -> %% means that field removals increment the clock too. -spec update_clock(riak_dt:actor() | riak_dt:dot(), riak_dt_vclock:vclock()) -> - {riak_dt:dot(), riak_dt_vclock:vclock()}. + {riak_dt:dot(), riak_dt_vclock:vclock()}. update_clock(Dot, Clock) when is_tuple(Dot) -> NewClock = riak_dt_vclock:merge([[Dot], Clock]), {Dot, NewClock}; @@ -309,7 +309,7 @@ update_clock(Actor, Clock) -> get({_Name, Type}=Field, Fields, Clock) -> CRDT = case dict:find(Field, Fields) of - {ok, {_Dots, CRDT0}} -> + {ok, {_Dots, CRDT0, _ContextT}} -> CRDT0; error -> Type:new() @@ -317,18 +317,18 @@ get({_Name, Type}=Field, Fields, Clock) -> Type:parent_clock(Clock, CRDT). get_entry({_Name, Type}=Field, Fields, Clock) -> - {Dots, CRDT} = case dict:find(Field, Fields) of - {ok, Entry} -> - Entry; - error -> - {[], Type:new()} - end, - {Dots, Type:parent_clock(Clock, CRDT)}. + {Dots, CRDT, ContextT} = case dict:find(Field, Fields) of + {ok, Entry} -> + Entry; + error -> + {[], Type:new(), riak_dt_vclock:fresh()} + end, + {Dots, Type:parent_clock(Clock, CRDT), ContextT}. %% @private -spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), {riak_dt_vclock:vclock(), entries() , deferred()}, context()) -> - {ok, map()} | precondition_error(). + {ok, map()} | precondition_error(). apply_ops([], _Dot, Map, _Ctx) -> {ok, Map}; apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> @@ -336,7 +336,7 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer case Type:update(Op, Dot, CRDT, Ctx) of {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), - NewValues = dict:store(Field, {[Dot], Updated}, Values), + NewValues = dict:store(Field, {[Dot], Updated, riak_dt_vclock:fresh()}, Values), apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); Error -> Error @@ -362,7 +362,7 @@ apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> %% @see defer_remove/4 for handling of removes of fields that are %% _not_ present -spec remove_field(field(), map(), context()) -> - {ok, map()} | precondition_error(). + {ok, map()} | precondition_error(). remove_field(Field, {Clock, Values, Deferred}, undefined) -> case dict:find(Field, Values) of error -> @@ -373,7 +373,12 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> %% Context removes remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), + DefCtx = get_def_ctx(dict:find(Field, Values),Clock), NewValues = case ctx_rem_field(Field, Values, Ctx, Clock) of + empty when length(DefCtx) > 0 -> + {ok, {Dots, CRDT, DeferredT}}=dict:find(Field, Values), + Append = riak_dt_vclock:merge([DefCtx,DeferredT]), + dict:store(Field,{Dots,CRDT,Append},Values); empty -> dict:erase(Field, Values); CRDT -> @@ -384,7 +389,11 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> %% @private drop dominated fields ctx_rem_field(_Field, error, _Ctx_, _Clock) -> empty; -ctx_rem_field({_, Type}, {ok, {Dots, CRDT}}, Ctx, MapClock) -> + +ctx_rem_field({_, riak_dt_map}, {ok, {_Dots, _CRDT,_DeferredT}}=_Elem, _Ctx, _MapClock) -> + io:format("Field is a map~n"); + +ctx_rem_field({_, Type}, {ok, {Dots, CRDT,_DeferredT}}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% %% If the context is removing a field at dot {a, 1} and the @@ -408,6 +417,19 @@ ctx_rem_field({_, Type}, {ok, {Dots, CRDT}}, Ctx, MapClock) -> ctx_rem_field(Field, Values, Ctx, MapClock) -> ctx_rem_field(Field, dict:find(Field, Values), Ctx, MapClock). +%% Case value is a CRDT +get_def_ctx({ok, {_, {_ValueClock,_Value,ValueDef},_MapEntryDef}}, MapClock) -> + %%ATTENTION: Merging the deferred clock with the map clock. + %% -- Its necessary to detect new updates. + %%This operations does not take into account the given context + %% -- Retrieves any deferred operation in the object + %%TODO: What to do with _MapEntryDef? + riak_dt_vclock:merge([MapClock | ValueDef]); + +get_def_ctx(error,_MapClock) -> + []. + + %% @private If we're asked to remove something we don't have (or have, %% but maybe not all 'updates' for it), is it because we've not seen %% the some update that we've been asked to remove, or is it because @@ -438,21 +460,23 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), Fields = lists:umerge(dict:fetch_keys(LHSEntries), dict:fetch_keys(RHSEntries)), Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> - {LHSDots, LHSCRDT} = get_entry(Field, LHSEntries, LHSClock), - {RHSDots, RHSCRDT} = get_entry(Field, RHSEntries, RHSClock), + {LHSDots, LHSCRDT, LHDeferredT} = get_entry(Field, LHSEntries, LHSClock), + {RHSDots, RHSCRDT, RHDeferredT} = get_entry(Field, RHSEntries, RHSClock), case keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) of [] -> Acc; Dots -> CRDT = Type:merge(LHSCRDT, RHSCRDT), + MergedTombstone = riak_dt_vclock:merge([LHDeferredT,RHDeferredT]), %% Yes! Reset the clock, again - dict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT)}, Acc) + dict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT), MergedTombstone}, Acc) end end, dict:new(), Fields), Deferred = merge_deferred(LHSDeferred, RHSDeferred), - apply_deferred(Clock, Entries, Deferred). + CRDT = apply_deferred(Clock, Entries, Deferred), + clear_deferred_tombstones(CRDT). keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> CommonDots = sets:intersection(sets:from_list(LHSDots), sets:from_list(RHSDots)), @@ -492,6 +516,27 @@ remove_all(Fields, Map, Ctx) -> Map, Fields). +%Eliminates the tombstone context if it has been integrated in the object's clock. +clear_deferred_tombstones({Clock, Entries, Deferred}) -> + FilteredEntries = ?DICT:fold(fun(Type, {Clocki, _CRDT, Tombstone}=Elem,Acc) -> + case riak_dt_vclock:descends(Clock, Tombstone) of + false -> + %%Clock does not descend from Tombstone - keep all. + dict:store(Type, Elem, Acc); + true -> + %%Clock descends from Tombstone, + case riak_dt_vclock:descends(Tombstone,Clocki) of + false -> + %%Clocki greater than tombstone -- Keep elem, clear tombstone + dict:store(Type, {Clocki, _CRDT, []},Acc); + true -> + %%Tombstone equal to clocki -- Remove entry + Acc + end + end + end, dict:new(), Entries), + {Clock,FilteredEntries,Deferred}. + %% @doc compare two `map()'s for equality of structure Both schemas %% and value list must be equal. Performs a pariwise equals for all %% values in the value lists From fbe2575e08ad8a1e1766507cd6c37ca1d59e7abf Mon Sep 17 00:00:00 2001 From: balegas Date: Sun, 4 Jan 2015 22:04:19 +0000 Subject: [PATCH 05/33] Added remove subtree test --- src/riak_dt_map.erl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index b70cbdc..c043641 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -704,6 +704,21 @@ keep_deferred_with_concurrent_add_test() -> StateAB = merge(StateA2,StateB2), ?assertEqual([{{'X',riak_dt_od_flag},true}],value(StateAB)). +remove_subtree_test() -> + Field = {'X', riak_dt_map}, + FieldA = {'X.A', riak_dt_od_flag}, + + EnableFlagA = {update, [{update, Field, {update, [{update, FieldA, enable}]}}]}, + DisableFlagA = {update, [{update, Field, {update, [{update, FieldA, disable}]}}]}, + RemoveFieldX = {update, [{remove, Field}]}, + + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(EnableFlagA, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(DisableFlagA, b, InitialState, CtxA1), + {ok, StateB2} = update(RemoveFieldX, b, StateB1, CtxB1), + StateAB = merge(StateA1,StateB2), + ?assertEqual([],value(StateAB)). + %% This fails on previous version of riak_dt_map assoc_test() -> Field = {'X', riak_dt_orswot}, From 42ea8c1c324e66c7db6cc1131f65b4af6e64f45b Mon Sep 17 00:00:00 2001 From: balegas Date: Sun, 4 Jan 2015 22:11:57 +0000 Subject: [PATCH 06/33] Work in progress: Added remove subtree Missing: - Control visibility of elements that have tombstones (recursive function, may be expensive) - Add context to remove subtree --- src/riak_dt_map.erl | 67 +++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index c043641..9c40aa4 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -373,16 +373,18 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> %% Context removes remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), - DefCtx = get_def_ctx(dict:find(Field, Values),Clock), - NewValues = case ctx_rem_field(Field, Values, Ctx, Clock) of - empty when length(DefCtx) > 0 -> + %%TODO: Does it need context argument? + %% -- Yes: A context that does not observe a deferred, should not move it upstream. + {DefCtx, UpdtValues} = remove_subtree(Field, Values, Clock), + NewValues = case ctx_rem_field(Field, UpdtValues, Ctx, Clock) of + empty when DefCtx =/= no_deferred -> {ok, {Dots, CRDT, DeferredT}}=dict:find(Field, Values), - Append = riak_dt_vclock:merge([DefCtx,DeferredT]), - dict:store(Field,{Dots,CRDT,Append},Values); + Tombstone = riak_dt_vclock:merge([DefCtx,DeferredT]), + dict:store(Field,{Dots, CRDT, Tombstone},UpdtValues); empty -> - dict:erase(Field, Values); + dict:erase(Field, UpdtValues); CRDT -> - dict:store(Field, CRDT, Values) + dict:store(Field, CRDT, UpdtValues) end, {ok, {Clock, NewValues, Deferred}}. @@ -390,9 +392,6 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> ctx_rem_field(_Field, error, _Ctx_, _Clock) -> empty; -ctx_rem_field({_, riak_dt_map}, {ok, {_Dots, _CRDT,_DeferredT}}=_Elem, _Ctx, _MapClock) -> - io:format("Field is a map~n"); - ctx_rem_field({_, Type}, {ok, {Dots, CRDT,_DeferredT}}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% @@ -417,18 +416,47 @@ ctx_rem_field({_, Type}, {ok, {Dots, CRDT,_DeferredT}}, Ctx, MapClock) -> ctx_rem_field(Field, Values, Ctx, MapClock) -> ctx_rem_field(Field, dict:find(Field, Values), Ctx, MapClock). -%% Case value is a CRDT -get_def_ctx({ok, {_, {_ValueClock,_Value,ValueDef},_MapEntryDef}}, MapClock) -> +%% Value is a map: +%% Remove fields that don't have deferred operations. +%% Get contexts from fields that have and update context tombstones. +remove_subtree({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, DeferredT}, MapClock)-> + {SubMergedDef, SubEntries} = dict:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> + case remove_subtree(K, V, MapClock) of + %%ATTENTION: Should remove entries that have no deferred ops. + %%Leaving them in place for now. + %%{MapClock, V} -> + %%%No deferred context in subtree + %%% - remove entry + %%{UpdtClock, UpdtEntries}; + {TombstoneClock, Value} -> + %%Some deferred operation in subtree + %% - keep entry, update tombstones + {riak_dt_vclock:merge([TombstoneClock | UpdtClock]), + dict:store(K, Value, UpdtEntries)} + end + end, {riak_dt_vclock:fresh(),dict:new()},Value0), + {SubMergedDef, {Dots, {ValueClock, SubEntries, ValueDef}, riak_dt_vclock:merge([SubMergedDef | DeferredT])}}; + +%% Value is a leaf: +%% Merge deferred operations' context with MapClock and send it upstream +remove_subtree(_, {_, {_ValueClock,_Value,ValueDef},_MapEntryDef}=Entry, MapClock) -> %%ATTENTION: Merging the deferred clock with the map clock. %% -- Its necessary to detect new updates. %%This operations does not take into account the given context %% -- Retrieves any deferred operation in the object %%TODO: What to do with _MapEntryDef? - riak_dt_vclock:merge([MapClock | ValueDef]); - -get_def_ctx(error,_MapClock) -> - []. + {riak_dt_vclock:merge([MapClock | ValueDef]), Entry}; +remove_subtree(Field, Values, MapClock) -> + case dict:find(Field, Values) of + {ok,Value} -> + {UpdtClock,UpdtValue} = remove_subtree(Field, Value, MapClock), + case UpdtClock of + MapClock -> {no_deferred, Values}; + _ -> {UpdtClock, dict:store(Field, UpdtValue, Values)} + end; + error -> {MapClock, Values} + end. %% @private If we're asked to remove something we don't have (or have, %% but maybe not all 'updates' for it), is it because we've not seen @@ -702,7 +730,7 @@ keep_deferred_with_concurrent_add_test() -> %Remove field in B is causal with the disable op - it removes the field and the deferred. {ok, StateB2} = update({update, [{remove, Field}]}, b, StateB1, CtxB1), StateAB = merge(StateA2,StateB2), - ?assertEqual([{{'X',riak_dt_od_flag},true}],value(StateAB)). + ?assertEqual([{{'X',riak_dt_od_flag},true}],value(StateAB)). remove_subtree_test() -> Field = {'X', riak_dt_map}, @@ -719,6 +747,11 @@ remove_subtree_test() -> StateAB = merge(StateA1,StateB2), ?assertEqual([],value(StateAB)). +keep_subtree_with_concurrent_add_test() -> + false. + +%%TODO: Visibility test; Remove with a context that does not cover a deferred operation --- define precise semantics. + %% This fails on previous version of riak_dt_map assoc_test() -> Field = {'X', riak_dt_orswot}, From 92d05388fbfd532998c96440882902ded3ad0ad7 Mon Sep 17 00:00:00 2001 From: balegas Date: Sun, 4 Jan 2015 22:48:52 +0000 Subject: [PATCH 07/33] Fixed Map equals --- src/riak_dt_map.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 9c40aa4..7f47ceb 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -578,9 +578,9 @@ equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> -spec pairwise_equals(entries(), entries()) -> boolean(). pairwise_equals([], []) -> true; -pairwise_equals([{{Name, Type}, {Dots1, CRDT1}}|Rest1], [{{Name, Type}, {Dots2, CRDT2}}|Rest2]) -> - case {riak_dt_vclock:equal(Dots1, Dots2), Type:equal(CRDT1, CRDT2)} of - {true, true} -> +pairwise_equals([{{Name, Type}, {Dots1, CRDT1,DeferredT1}}|Rest1], [{{Name, Type}, {Dots2, CRDT2, DeferredT2}}|Rest2]) -> + case {riak_dt_vclock:equal(Dots1, Dots2), Type:equal(CRDT1, CRDT2), riak_dt_vclock:equal(DeferredT1, DeferredT2)} of + {true, true, true} -> pairwise_equals(Rest1, Rest2); _ -> false From db9d3c6aa91024f8f09fdf6a51ce8e63348316fd Mon Sep 17 00:00:00 2001 From: balegas Date: Mon, 5 Jan 2015 18:11:27 +0000 Subject: [PATCH 08/33] Stuff not to forget --- src/riak_dt_map.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 7f47ceb..a9c87c7 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -546,6 +546,7 @@ remove_all(Fields, Map, Ctx) -> %Eliminates the tombstone context if it has been integrated in the object's clock. clear_deferred_tombstones({Clock, Entries, Deferred}) -> + %%TODO: check if _CRDT is a map and call the clear recursively FilteredEntries = ?DICT:fold(fun(Type, {Clocki, _CRDT, Tombstone}=Elem,Acc) -> case riak_dt_vclock:descends(Clock, Tombstone) of false -> @@ -750,7 +751,7 @@ remove_subtree_test() -> keep_subtree_with_concurrent_add_test() -> false. -%%TODO: Visibility test; Remove with a context that does not cover a deferred operation --- define precise semantics. +%%TODO: Visibility test; Remove with a context that does not cover a deferred operation --- define precise semantics; Create X.F1 and X.F2. put a deferred in X.F1 and set X.F2 to true. Remove X. value must be []. concurrently set X.F1 to true and merge. X.F1 should be true and X.F2 should not exist. %% This fails on previous version of riak_dt_map assoc_test() -> From 5291c074ce65c56bdacf34db27f3bdc968461643 Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 6 Jan 2015 09:40:03 +0000 Subject: [PATCH 09/33] Tests for field removes in a subtree. --- src/riak_dt_map.erl | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index a9c87c7..0754ce2 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -748,6 +748,39 @@ remove_subtree_test() -> StateAB = merge(StateA1,StateB2), ?assertEqual([],value(StateAB)). +remove_entry_in_subtree_test() -> + Field = {'X', riak_dt_map}, + FieldA = {'X.A', riak_dt_od_flag}, + + EnableFlagA = {update, [{update, Field, {update, [{update, FieldA, enable}]}}]}, + DisableFlagA = {update, [{update, Field, {update, [{update, FieldA, disable}]}}]}, + RemoveFieldXA = {update, [{update, Field, {update, [{remove, FieldA}]}}]}, + + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(EnableFlagA, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(DisableFlagA, b, InitialState, CtxA1), + {ok, StateB2} = update(RemoveFieldXA, b, StateB1, CtxB1), + StateAB = merge(StateA1,StateB2), + ?assertEqual([{{'X',riak_dt_map},[]}],value(StateAB)). + +remove_entry_in_subtree_2_test() -> + Field = {'X', riak_dt_map}, + FieldA = {'X.A', riak_dt_od_flag}, + FieldB = {'X.B', riak_dt_od_flag}, + + EnableFlagA = {update, [{update, Field, {update, [{update, FieldA, enable}]}}]}, + EnableFlagB = {update, [{update, Field, {update, [{update, FieldB, enable}]}}]}, + DisableFlagA = {update, [{update, Field, {update, [{update, FieldA, disable}]}}]}, + RemoveFieldXA = {update, [{update, Field, {update, [{remove, FieldA}]}}]}, + + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(EnableFlagA, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(DisableFlagA, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = update(EnableFlagB, b, StateB1, CtxB1), + {ok, StateB3} = update(RemoveFieldXA, b, StateB2, CtxB2), + StateAB = merge(StateA1,StateB2), + ?assertEqual([{{'X',riak_dt_map}, {{'X.B',riak_dt_od_flag},true}]}],[]}],value(StateAB)). + keep_subtree_with_concurrent_add_test() -> false. From 8e3f4e00acb121c24c0450491c73dde7c804459f Mon Sep 17 00:00:00 2001 From: balegas Date: Wed, 7 Jan 2015 18:54:59 +0000 Subject: [PATCH 10/33] Tests updated --- src/riak_dt_map.erl | 140 +++++++++++++++++++++++++++++--------------- 1 file changed, 92 insertions(+), 48 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 0754ce2..c63f220 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -706,85 +706,129 @@ from_binary(?V2_VERS, <>) -> %% =================================================================== -ifdef(TEST). +-define(FIELD, {'X', riak_dt_map}). +-define(FIELD_X, {'X', riak_dt_od_flag}). +-define(FIELD_A, {'X.A', riak_dt_od_flag}). +-define(FIELD_B, {'X.B', riak_dt_od_flag}). + +-define(ENABLE_FLAG_A, {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}]}}]}). +-define(ENABLE_FLAG_B, {update, [{update, ?FIELD, {update, [{update, ?FIELD_B, enable}]}}]}). +-define(DISABLE_FLAG_A, {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, disable}]}}]}). +-define(DISABLE_FLAG_B, {update, [{update, ?FIELD, {update, [{update, ?FIELD_B, disable}]}}]}). + +-define(REMOVE_FIELD_X,{update, [{remove, ?FIELD}]}). +-define(REMOVE_FIELD_XA, {update, [{update, ?FIELD, {update, [{remove, ?FIELD_A}]}}]}). +-define(REMOVE_FIELD_XB, {update, [{update, ?FIELD, {update, [{remove, ?FIELD_B}]}}]}). + + +%% Issue 99 test case keep_deferred_test() -> - Field = {'X', riak_dt_od_flag}, InitialState = riak_dt_map:new(), %Update at node A - {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, Field, enable}]}, a, InitialState), + {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), %Update with the context of node A on node B generates a deferred. - {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, Field, disable}]}, b, InitialState, CtxA1), + {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, ?FIELD_X, disable}]}, b, InitialState, CtxA1), %Remove field in B is causal with the disable op - it removes the field and the deferred. - {ok, StateB2} = update({update, [{remove, Field}]}, b, StateB1, CtxB1), + {ok, StateB2} = update({update, [{remove, ?FIELD_X}]}, b, StateB1, CtxB1), StateAB = merge(StateA1,StateB2), ?assertEqual([],value(StateAB)). +%% Test that the field is preserved if a deferred arrives after a remove +keep_multiple_deferred_test() -> + InitialState = riak_dt_map:new(), + + %Update at node A + {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), + %Update with the context of node A on node B generates a deferred. + {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, ?FIELD_X, disable}]}, b, InitialState, CtxA1), + %Remove field in B is causal with the disable op - it removes the field and the deferred. + {ok, StateB2} = update({update, [{remove, ?FIELD_X}]}, b, StateB1, CtxB1), + {ok, StateB3} = update({update, [{update, ?FIELD_X, disable}]}, b, StateB2, [{c,1}]), + StateAB = merge(StateA1,StateB3), + ?assertEqual([{{'X',riak_dt_od_flag},false}],value(StateAB)). + +%% Concurrently enable the flag keep_deferred_with_concurrent_add_test() -> - Field = {'X', riak_dt_od_flag}, InitialState = riak_dt_map:new(), %Update at node A - {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, Field, enable}]}, a, InitialState), + {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), %Update with the context of node A on node B generates a deferred. - {ok, {_, _, _}=StateA2} = update({update, [{update, Field, enable}]}, a, StateA1, CtxA1), - {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, Field, disable}]}, b, InitialState, CtxA1), + {ok, {_, _, _}=StateA2} = update({update, [{update, ?FIELD_X, enable}]}, a, StateA1, CtxA1), + {ok, {CtxB1, _, _}=StateB1} = update({update, [{update, ?FIELD_X, disable}]}, b, InitialState, CtxA1), %Remove field in B is causal with the disable op - it removes the field and the deferred. - {ok, StateB2} = update({update, [{remove, Field}]}, b, StateB1, CtxB1), + {ok, StateB2} = update({update, [{remove, ?FIELD_X}]}, b, StateB1, CtxB1), StateAB = merge(StateA2,StateB2), ?assertEqual([{{'X',riak_dt_od_flag},true}],value(StateAB)). +%% Remove a field that is a map with a deferred operation. +%% Map should be removed in the end remove_subtree_test() -> - Field = {'X', riak_dt_map}, - FieldA = {'X.A', riak_dt_od_flag}, - - EnableFlagA = {update, [{update, Field, {update, [{update, FieldA, enable}]}}]}, - DisableFlagA = {update, [{update, Field, {update, [{update, FieldA, disable}]}}]}, - RemoveFieldX = {update, [{remove, Field}]}, - InitialState = new(), - {ok, {CtxA1,_,_}=StateA1} = update(EnableFlagA, a, InitialState), - {ok, {CtxB1,_,_}=StateB1} = update(DisableFlagA, b, InitialState, CtxA1), - {ok, StateB2} = update(RemoveFieldX, b, StateB1, CtxB1), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, StateB2} = update(?REMOVE_FIELD_X, b, StateB1, CtxB1), StateAB = merge(StateA1,StateB2), ?assertEqual([],value(StateAB)). +%% Remove a field inside a map that has a deferred operation. remove_entry_in_subtree_test() -> - Field = {'X', riak_dt_map}, - FieldA = {'X.A', riak_dt_od_flag}, - - EnableFlagA = {update, [{update, Field, {update, [{update, FieldA, enable}]}}]}, - DisableFlagA = {update, [{update, Field, {update, [{update, FieldA, disable}]}}]}, - RemoveFieldXA = {update, [{update, Field, {update, [{remove, FieldA}]}}]}, - InitialState = new(), - {ok, {CtxA1,_,_}=StateA1} = update(EnableFlagA, a, InitialState), - {ok, {CtxB1,_,_}=StateB1} = update(DisableFlagA, b, InitialState, CtxA1), - {ok, StateB2} = update(RemoveFieldXA, b, StateB1, CtxB1), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, StateB2} = update(?REMOVE_FIELD_XA, b, StateB1, CtxB1), StateAB = merge(StateA1,StateB2), ?assertEqual([{{'X',riak_dt_map},[]}],value(StateAB)). +%% Remove a field X that is a map that has a entry with a deferred +%% and a flag that is enable after remove X but before receiving the deferred. +%% The idea is to test that the flag is preserved after clearing the tombstones remove_entry_in_subtree_2_test() -> - Field = {'X', riak_dt_map}, - FieldA = {'X.A', riak_dt_od_flag}, - FieldB = {'X.B', riak_dt_od_flag}, - - EnableFlagA = {update, [{update, Field, {update, [{update, FieldA, enable}]}}]}, - EnableFlagB = {update, [{update, Field, {update, [{update, FieldB, enable}]}}]}, - DisableFlagA = {update, [{update, Field, {update, [{update, FieldA, disable}]}}]}, - RemoveFieldXA = {update, [{update, Field, {update, [{remove, FieldA}]}}]}, - InitialState = new(), - {ok, {CtxA1,_,_}=StateA1} = update(EnableFlagA, a, InitialState), - {ok, {CtxB1,_,_}=StateB1} = update(DisableFlagA, b, InitialState, CtxA1), - {ok, {CtxB2,_,_}=StateB2} = update(EnableFlagB, b, StateB1, CtxB1), - {ok, StateB3} = update(RemoveFieldXA, b, StateB2, CtxB2), - StateAB = merge(StateA1,StateB2), - ?assertEqual([{{'X',riak_dt_map}, {{'X.B',riak_dt_od_flag},true}]}],[]}],value(StateAB)). - -keep_subtree_with_concurrent_add_test() -> - false. + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = update(?REMOVE_FIELD_X, b, StateB1, CtxB1), + {ok, StateB3} = update(?ENABLE_FLAG_B, b, StateB2, CtxB2), + StateAB = merge(StateA1,StateB3), + ?assertEqual([{{'X',riak_dt_map}, [{{'X.B',riak_dt_od_flag},true}]}],value(StateAB)). + +%% The same as before, but the enable flag is a remote operation. +remove_entry_in_subtree_3_test() -> + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {_,_,_}=StateB2} = update(?REMOVE_FIELD_X, b, StateB1, CtxB1), + {ok, StateA2} = update(?ENABLE_FLAG_B, a, StateA1, CtxA1), + StateAB = merge(StateA2,StateB2), + ?assertEqual([{{'X',riak_dt_map}, [{{'X.B',riak_dt_od_flag},true}]}],value(StateAB)). -%%TODO: Visibility test; Remove with a context that does not cover a deferred operation --- define precise semantics; Create X.F1 and X.F2. put a deferred in X.F1 and set X.F2 to true. Remove X. value must be []. concurrently set X.F1 to true and merge. X.F1 should be true and X.F2 should not exist. +two_deferred_entries_test() -> + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxC1,_,_}=StateC1} = update(?ENABLE_FLAG_B, c, InitialState), + {ok, {_CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = update(?DISABLE_FLAG_B, b, StateB1, CtxC1), + {ok, {CtxB3,_,_}=StateB3} = update(?REMOVE_FIELD_XA, b, StateB2, CtxB2), + {ok, {_CtxB4,_,_}=StateB4} = update(?REMOVE_FIELD_XB, b, StateB3, CtxB3), + StateAB = merge(StateA1,StateB4), + ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},false}]}],value(StateAB)), + StateABC = merge(StateAB,StateC1), + ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). + +two_deferred_entries_2_test() -> + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxC1,_,_}=StateC1} = update(?ENABLE_FLAG_B, c, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {_CtxB2,_,_}=StateB2} = update(?REMOVE_FIELD_XA, b, StateB1, CtxB1), + {ok, {CtxB3,_,_}=StateB3} = update(?DISABLE_FLAG_B, b, StateB2, CtxC1), + StateAB = merge(StateA1,StateB3), + {ok, {_,_,_}=StateAB1} = update(?REMOVE_FIELD_XB, b, StateAB, CtxB3), + StateABC = merge(StateAB1,StateC1), + ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). + +%%TODO: Visibility test; Remove with a context that does not cover a deferred operation; %% This fails on previous version of riak_dt_map assoc_test() -> From 7810b073b76ba20ec7830e839067737a06a9eb91 Mon Sep 17 00:00:00 2001 From: balegas Date: Wed, 7 Jan 2015 18:57:06 +0000 Subject: [PATCH 11/33] WIP - modified merge and a few things to correct some cases that failed in tests. --- src/riak_dt_map.erl | 175 ++++++++++++++++++++++++++++++-------------- 1 file changed, 121 insertions(+), 54 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index c63f220..763fae8 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -336,7 +336,12 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer case Type:update(Op, Dot, CRDT, Ctx) of {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), - NewValues = dict:store(Field, {[Dot], Updated, riak_dt_vclock:fresh()}, Values), + NewValues = case dict:find(Field,Values) of + {ok, {_,_,Tombstone}} -> + dict:store(Field, {[Dot], Updated, Tombstone}, Values); + error -> + dict:store(Field, {[Dot], Updated, riak_dt_vclock:fresh()}, Values) + end, apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); Error -> Error @@ -373,14 +378,13 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> %% Context removes remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), - %%TODO: Does it need context argument? - %% -- Yes: A context that does not observe a deferred, should not move it upstream. - {DefCtx, UpdtValues} = remove_subtree(Field, Values, Clock), + {DefCtx, UpdtValues} = remove_subtree(Field, Values, Clock, Ctx), NewValues = case ctx_rem_field(Field, UpdtValues, Ctx, Clock) of empty when DefCtx =/= no_deferred -> - {ok, {Dots, CRDT, DeferredT}}=dict:find(Field, Values), - Tombstone = riak_dt_vclock:merge([DefCtx,DeferredT]), - dict:store(Field,{Dots, CRDT, Tombstone},UpdtValues); + dict:update(Field, fun({Dots, CRDT, DeferredT}) -> + Tombstone = riak_dt_vclock:merge([DefCtx,DeferredT]), + {Dots, CRDT, Tombstone} + end,UpdtValues); empty -> dict:erase(Field, UpdtValues); CRDT -> @@ -392,7 +396,7 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> ctx_rem_field(_Field, error, _Ctx_, _Clock) -> empty; -ctx_rem_field({_, Type}, {ok, {Dots, CRDT,_DeferredT}}, Ctx, MapClock) -> +ctx_rem_field({_, Type}, {ok, {Dots, CRDT, DeferredT}}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% %% If the context is removing a field at dot {a, 1} and the @@ -411,7 +415,7 @@ ctx_rem_field({_, Type}, {ok, {Dots, CRDT,_DeferredT}}, Ctx, MapClock) -> %% Update the tombstone with the GLB clock CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), %% Always reset to empty clock so we don't duplicate storage - {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2)} + {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2), DeferredT} end; ctx_rem_field(Field, Values, Ctx, MapClock) -> ctx_rem_field(Field, dict:find(Field, Values), Ctx, MapClock). @@ -419,38 +423,44 @@ ctx_rem_field(Field, Values, Ctx, MapClock) -> %% Value is a map: %% Remove fields that don't have deferred operations. %% Get contexts from fields that have and update context tombstones. -remove_subtree({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, DeferredT}, MapClock)-> - {SubMergedDef, SubEntries} = dict:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> - case remove_subtree(K, V, MapClock) of - %%ATTENTION: Should remove entries that have no deferred ops. - %%Leaving them in place for now. - %%{MapClock, V} -> - %%%No deferred context in subtree - %%% - remove entry - %%{UpdtClock, UpdtEntries}; - {TombstoneClock, Value} -> - %%Some deferred operation in subtree - %% - keep entry, update tombstones - {riak_dt_vclock:merge([TombstoneClock | UpdtClock]), - dict:store(K, Value, UpdtEntries)} - end - end, {riak_dt_vclock:fresh(),dict:new()},Value0), - {SubMergedDef, {Dots, {ValueClock, SubEntries, ValueDef}, riak_dt_vclock:merge([SubMergedDef | DeferredT])}}; +remove_subtree({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, DeferredT}, MapClock, Ctx)-> + {SubMergedDef, SubEntries} = + dict:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> + case remove_subtree(K, V, MapClock, Ctx) of + {_,empty} -> + {UpdtClock, UpdtEntries}; + {TombstoneClock, Value} -> + %%Some deferred operation in subtree + %% - keep entry, update tombstones + {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), + dict:store(K, Value, UpdtEntries)} + end + end, {riak_dt_vclock:fresh(),dict:new()},Value0), + %Clear map if all entries are empty + case dict:size(SubEntries) of + 0 -> + {SubMergedDef,empty}; + _ -> {SubMergedDef, {Dots, {ValueClock, SubEntries, ValueDef}, + riak_dt_vclock:merge([SubMergedDef | DeferredT])}} + end + ; %% Value is a leaf: %% Merge deferred operations' context with MapClock and send it upstream -remove_subtree(_, {_, {_ValueClock,_Value,ValueDef},_MapEntryDef}=Entry, MapClock) -> - %%ATTENTION: Merging the deferred clock with the map clock. - %% -- Its necessary to detect new updates. - %%This operations does not take into account the given context - %% -- Retrieves any deferred operation in the object - %%TODO: What to do with _MapEntryDef? - {riak_dt_vclock:merge([MapClock | ValueDef]), Entry}; - -remove_subtree(Field, Values, MapClock) -> +remove_subtree(Field, {Clock, {_ValueClock,_Value,ValueDef}=CRDT,TombstoneIn}, MapClock, Ctx) -> + %%TODO: What to do when value def is not a list? Execute the deferred op? + case ValueDef of + [] -> + {[], ctx_rem_field(Field, {ok, {Clock, CRDT, []}}, Ctx, MapClock)}; + _ -> + Tombstone = riak_dt_vclock:merge([Clock, TombstoneIn | ValueDef]), + {Tombstone, {Clock, CRDT, Tombstone}} + end; + +remove_subtree(Field, Values, MapClock, Ctx) -> case dict:find(Field, Values) of {ok,Value} -> - {UpdtClock,UpdtValue} = remove_subtree(Field, Value, MapClock), + {UpdtClock,UpdtValue} = remove_subtree(Field, Value, MapClock, Ctx), case UpdtClock of MapClock -> {no_deferred, Values}; _ -> {UpdtClock, dict:store(Field, UpdtValue, Values)} @@ -546,26 +556,36 @@ remove_all(Fields, Map, Ctx) -> %Eliminates the tombstone context if it has been integrated in the object's clock. clear_deferred_tombstones({Clock, Entries, Deferred}) -> - %%TODO: check if _CRDT is a map and call the clear recursively - FilteredEntries = ?DICT:fold(fun(Type, {Clocki, _CRDT, Tombstone}=Elem,Acc) -> - case riak_dt_vclock:descends(Clock, Tombstone) of - false -> - %%Clock does not descend from Tombstone - keep all. - dict:store(Type, Elem, Acc); - true -> - %%Clock descends from Tombstone, - case riak_dt_vclock:descends(Tombstone,Clocki) of - false -> - %%Clocki greater than tombstone -- Keep elem, clear tombstone - dict:store(Type, {Clocki, _CRDT, []},Acc); - true -> - %%Tombstone equal to clocki -- Remove entry - Acc - end - end - end, dict:new(), Entries), + FilteredEntries = + ?DICT:fold(fun(Type, Value, Acc) -> + clear_deferred_tombstones_handle(Type,Value,Acc,Clock) + end, dict:new(), Entries), {Clock,FilteredEntries,Deferred}. +clear_deferred_tombstones_handle({_, riak_dt_map}=Field, {MapDots, {C, CRDT, D}, T}, Acc, MapClock) -> + FilteredEntries = + ?DICT:fold(fun(Type, Value, AccMap) -> + clear_deferred_tombstones_handle(Type, Value, AccMap, MapClock) + end, dict:new(), CRDT), + case dict:size(FilteredEntries) == 0 of + true when length(T) > 0 -> Acc; + _ -> dict:store(Field, {MapDots, {C,FilteredEntries,D}, T},Acc) + end; + +clear_deferred_tombstones_handle(Type, {MapDots, CRDT, Tombstone}, Acc, MapClock) -> + ObjDominatesTomb = riak_dt_vclock:dominates(MapDots, Tombstone), + case ObjDominatesTomb of + true -> + dict:store(Type, {MapDots, CRDT, []}, Acc); + false -> + MapDescends = riak_dt_vclock:descends(MapClock, Tombstone), + case MapDescends of + true -> + Acc; + false -> dict:store(Type, {MapDots, CRDT, Tombstone}, Acc) + end + end. + %% @doc compare two `map()'s for equality of structure Both schemas %% and value list must be equal. Performs a pariwise equals for all %% values in the value lists @@ -707,6 +727,7 @@ from_binary(?V2_VERS, <>) -> -ifdef(TEST). -define(FIELD, {'X', riak_dt_map}). +-define(FIELD_Y, {'Y', riak_dt_map}). -define(FIELD_X, {'X', riak_dt_od_flag}). -define(FIELD_A, {'X.A', riak_dt_od_flag}). -define(FIELD_B, {'X.B', riak_dt_od_flag}). @@ -720,6 +741,9 @@ from_binary(?V2_VERS, <>) -> -define(REMOVE_FIELD_XA, {update, [{update, ?FIELD, {update, [{remove, ?FIELD_A}]}}]}). -define(REMOVE_FIELD_XB, {update, [{update, ?FIELD, {update, [{remove, ?FIELD_B}]}}]}). +-define(ENABLE_FLAG_XYA, {update, [{update, ?FIELD, {update, [{update, ?FIELD_Y, {update, [{update, ?FIELD_A, enable}]}}]}}]}). + + %% Issue 99 test case keep_deferred_test() -> @@ -762,6 +786,17 @@ keep_deferred_with_concurrent_add_test() -> StateAB = merge(StateA2,StateB2), ?assertEqual([{{'X',riak_dt_od_flag},true}],value(StateAB)). +%% Remove using a context that does not descend from the object. +%% Remove concurrent with deferred delete --- preserve remove +keep_deferred_context_test() -> + InitialState = riak_dt_map:new(), + {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), + {ok, {_, _, _}=StateB1} = update({update, [{update, ?FIELD_X, disable}]}, b, InitialState, CtxA1), + {ok, StateB2} = update({update, [{remove, ?FIELD_X}]}, b, StateB1, CtxA1), + ?assertEqual([{{'X',riak_dt_od_flag},false}],value(StateB2)), + StateAB = merge(StateA1,StateB2), + ?assertEqual([{{'X',riak_dt_od_flag},false}],value(StateAB)). + %% Remove a field that is a map with a deferred operation. %% Map should be removed in the end remove_subtree_test() -> @@ -828,6 +863,38 @@ two_deferred_entries_2_test() -> StateABC = merge(StateAB1,StateC1), ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). +clear_invisible_after_merge_test() -> + InitialState = riak_dt_map:new(), + {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), + {ok, {_,_,_}=StateA2} = riak_dt_map:update(?ENABLE_FLAG_A, a, StateA1), + {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_B, b, StateB1, CtxB1), + {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + StateAB1 = riak_dt_map:merge(StateB3,StateA1), + riak_dt_map:value(StateAB1), + StateAB2 = riak_dt_map:merge(StateB3,StateA2), + riak_dt_map:value(StateAB2). + +clear_invisible_after_merge_2_test() -> + InitialState = riak_dt_map:new(), + {ok, {CtxA1,_,_}=_StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), + {ok, {_CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_B, b, InitialState,CtxA1), + {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_XYA, b, StateB1), + {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},false}]}], value(StateB3)). + +%%Set should only have element 1. +%clear_invisible_after_merge_set_test() -> +% AddElemToB = fun(Elem) -> +% {update, [{update, ?FIELD, {update, [{update, ?FIELD_B, {add, Elem}}]}}]} +% end, +% InitialState = riak_dt_map:new(), +% {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), +% {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_A, b, InitialState, CtxA1), +% {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(AddElemToB(0), b, StateB1, CtxB1), +% {ok, {CtxB3,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), +% {ok, {CtxB4,_,_}=StateB4} = riak_dt_map:update(AddElemToB(1), b, StateB3, CtxB3), + %%TODO: Visibility test; Remove with a context that does not cover a deferred operation; %% This fails on previous version of riak_dt_map From d9f35edc68c96d00e8410caf6f5f01829b55f59f Mon Sep 17 00:00:00 2001 From: balegas Date: Fri, 9 Jan 2015 15:47:28 +0000 Subject: [PATCH 12/33] Added visibility to value(), changed tests accordingly; Started refactoring --- src/riak_dt_map.erl | 133 ++++++++++++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 763fae8..555562a 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -251,12 +251,38 @@ parent_clock(Clock, {_MapClock, Values, Deferred}) -> %% @doc get the current set of values for this Map -spec value(map()) -> values(). value({_Clock, Values, _Deferred}) -> - lists:sort(dict:fold(fun({Name, Type}, {_Dots, CRDT, _ContextT}, Acc) -> - [{{Name, Type}, Type:value(CRDT)} | Acc] end, - [], - Values)). + %%Selects the visible elements from the map + lists:sort(dict:fold( + fun({Name, Type}, {Dots, CRDT, Tombstone}, Acc) -> + case Tombstone of + [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; + _ -> + case riak_dt_vclock:descends(Tombstone, Dots) of + %No new dots in this branch + true -> + Acc; + %Dots in the branch + false -> + case Type of + %Recursive search + riak_dt_map -> + SubTree = value(CRDT), + case SubTree of + [] -> + Acc; + _ -> [{{Name, Type}, Type:value(CRDT)} | Acc] + end; + _ -> + [{{Name, Type}, Type:value(CRDT)} | Acc] + end + end + end + end, + [], + Values)). %% @doc query map (not implemented yet) +%% -spec value(term(), map()) -> values(). value(_, Map) -> value(Map). @@ -309,7 +335,7 @@ update_clock(Actor, Clock) -> get({_Name, Type}=Field, Fields, Clock) -> CRDT = case dict:find(Field, Fields) of - {ok, {_Dots, CRDT0, _ContextT}} -> + {ok, {_Dots, CRDT0, _Tombstone}} -> CRDT0; error -> Type:new() @@ -317,13 +343,13 @@ get({_Name, Type}=Field, Fields, Clock) -> Type:parent_clock(Clock, CRDT). get_entry({_Name, Type}=Field, Fields, Clock) -> - {Dots, CRDT, ContextT} = case dict:find(Field, Fields) of + {Dots, CRDT, Tombstone} = case dict:find(Field, Fields) of {ok, Entry} -> Entry; error -> {[], Type:new(), riak_dt_vclock:fresh()} end, - {Dots, Type:parent_clock(Clock, CRDT), ContextT}. + {Dots, Type:parent_clock(Clock, CRDT), Tombstone}. %% @private -spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), @@ -375,14 +401,15 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> {ok, _Removed} -> {ok, {Clock, dict:erase(Field, Values), Deferred}} end; -%% Context removes + remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), - {DefCtx, UpdtValues} = remove_subtree(Field, Values, Clock, Ctx), + {DefCtx, UpdtValues} = propagate_remove(Field, Values, Clock, Ctx), NewValues = case ctx_rem_field(Field, UpdtValues, Ctx, Clock) of + %Element is removed but has deferred operations empty when DefCtx =/= no_deferred -> - dict:update(Field, fun({Dots, CRDT, DeferredT}) -> - Tombstone = riak_dt_vclock:merge([DefCtx,DeferredT]), + dict:update(Field, fun({Dots, CRDT, Tombstone}) -> + Tombstone = riak_dt_vclock:merge([DefCtx,Tombstone]), {Dots, CRDT, Tombstone} end,UpdtValues); empty -> @@ -396,7 +423,7 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> ctx_rem_field(_Field, error, _Ctx_, _Clock) -> empty; -ctx_rem_field({_, Type}, {ok, {Dots, CRDT, DeferredT}}, Ctx, MapClock) -> +ctx_rem_field({_, Type}, {ok, {Dots, CRDT, Tombstone}}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% %% If the context is removing a field at dot {a, 1} and the @@ -415,18 +442,18 @@ ctx_rem_field({_, Type}, {ok, {Dots, CRDT, DeferredT}}, Ctx, MapClock) -> %% Update the tombstone with the GLB clock CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), %% Always reset to empty clock so we don't duplicate storage - {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2), DeferredT} + {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2), Tombstone} end; ctx_rem_field(Field, Values, Ctx, MapClock) -> ctx_rem_field(Field, dict:find(Field, Values), Ctx, MapClock). %% Value is a map: -%% Remove fields that don't have deferred operations. -%% Get contexts from fields that have and update context tombstones. -remove_subtree({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, DeferredT}, MapClock, Ctx)-> +%% Remove fields that don't have deferred operations; +%% Compute the removal tombstone for this field. +propagate_remove({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, Tombstone}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = - dict:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> - case remove_subtree(K, V, MapClock, Ctx) of + dict:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> + case propagate_remove(K, V, MapClock, Ctx) of {_,empty} -> {UpdtClock, UpdtEntries}; {TombstoneClock, Value} -> @@ -441,26 +468,27 @@ remove_subtree({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, Deferred 0 -> {SubMergedDef,empty}; _ -> {SubMergedDef, {Dots, {ValueClock, SubEntries, ValueDef}, - riak_dt_vclock:merge([SubMergedDef | DeferredT])}} - end - ; + riak_dt_vclock:merge([SubMergedDef | Tombstone])}} + end; + %% Value is a leaf: %% Merge deferred operations' context with MapClock and send it upstream -remove_subtree(Field, {Clock, {_ValueClock,_Value,ValueDef}=CRDT,TombstoneIn}, MapClock, Ctx) -> +propagate_remove(Field, {Clock, {_ValueClock,_Value,ValueDef}=CRDT,TombstoneIn}, MapClock, Ctx) -> %%TODO: What to do when value def is not a list? Execute the deferred op? case ValueDef of [] -> {[], ctx_rem_field(Field, {ok, {Clock, CRDT, []}}, Ctx, MapClock)}; _ -> - Tombstone = riak_dt_vclock:merge([Clock, TombstoneIn | ValueDef]), + Intersection = riak_dt_vclock:glb(Clock,Ctx), + Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | ValueDef]), {Tombstone, {Clock, CRDT, Tombstone}} end; -remove_subtree(Field, Values, MapClock, Ctx) -> +propagate_remove(Field, Values, MapClock, Ctx) -> case dict:find(Field, Values) of {ok,Value} -> - {UpdtClock,UpdtValue} = remove_subtree(Field, Value, MapClock, Ctx), + {UpdtClock,UpdtValue} = propagate_remove(Field, Value, MapClock, Ctx), case UpdtClock of MapClock -> {no_deferred, Values}; _ -> {UpdtClock, dict:store(Field, UpdtValue, Values)} @@ -498,14 +526,14 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), Fields = lists:umerge(dict:fetch_keys(LHSEntries), dict:fetch_keys(RHSEntries)), Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> - {LHSDots, LHSCRDT, LHDeferredT} = get_entry(Field, LHSEntries, LHSClock), - {RHSDots, RHSCRDT, RHDeferredT} = get_entry(Field, RHSEntries, RHSClock), + {LHSDots, LHSCRDT, LHDTomb} = get_entry(Field, LHSEntries, LHSClock), + {RHSDots, RHSCRDT, RHDTomb} = get_entry(Field, RHSEntries, RHSClock), case keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) of [] -> Acc; Dots -> CRDT = Type:merge(LHSCRDT, RHSCRDT), - MergedTombstone = riak_dt_vclock:merge([LHDeferredT,RHDeferredT]), + MergedTombstone = riak_dt_vclock:merge([LHDTomb,RHDTomb]), %% Yes! Reset the clock, again dict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT), MergedTombstone}, Acc) end @@ -514,7 +542,7 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) Fields), Deferred = merge_deferred(LHSDeferred, RHSDeferred), CRDT = apply_deferred(Clock, Entries, Deferred), - clear_deferred_tombstones(CRDT). + clear_tombstones(CRDT). keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> CommonDots = sets:intersection(sets:from_list(LHSDots), sets:from_list(RHSDots)), @@ -554,35 +582,37 @@ remove_all(Fields, Map, Ctx) -> Map, Fields). -%Eliminates the tombstone context if it has been integrated in the object's clock. -clear_deferred_tombstones({Clock, Entries, Deferred}) -> +%Eliminates the tombstone if it has been integrated in the object's clock. +clear_tombstones({Clock, Entries, Deferred}) -> FilteredEntries = ?DICT:fold(fun(Type, Value, Acc) -> - clear_deferred_tombstones_handle(Type,Value,Acc,Clock) + clear_tombstones_handle(Type, Value, Acc, Clock) end, dict:new(), Entries), - {Clock,FilteredEntries,Deferred}. + {Clock, FilteredEntries, Deferred}. -clear_deferred_tombstones_handle({_, riak_dt_map}=Field, {MapDots, {C, CRDT, D}, T}, Acc, MapClock) -> +clear_tombstones_handle({_, riak_dt_map}=Field, {Dots, {Clock, CRDT, Deferred}, Tombstone}, Acc, MapClock) -> FilteredEntries = ?DICT:fold(fun(Type, Value, AccMap) -> - clear_deferred_tombstones_handle(Type, Value, AccMap, MapClock) + clear_tombstones_handle(Type, Value, AccMap, MapClock) end, dict:new(), CRDT), case dict:size(FilteredEntries) == 0 of - true when length(T) > 0 -> Acc; - _ -> dict:store(Field, {MapDots, {C,FilteredEntries,D}, T},Acc) + true when length(Tombstone) > 0 -> Acc; + _ -> dict:store(Field, {Dots, {Clock, FilteredEntries, Deferred}, Tombstone}, Acc) end; -clear_deferred_tombstones_handle(Type, {MapDots, CRDT, Tombstone}, Acc, MapClock) -> - ObjDominatesTomb = riak_dt_vclock:dominates(MapDots, Tombstone), +clear_tombstones_handle(Type, {Dots, CRDT, Tombstone}, Acc, MapClock) -> + ObjDominatesTomb = riak_dt_vclock:dominates(Dots, Tombstone), case ObjDominatesTomb of true -> - dict:store(Type, {MapDots, CRDT, []}, Acc); + %Object received concurrent update + dict:store(Type, {Dots, CRDT, []}, Acc); false -> MapDescends = riak_dt_vclock:descends(MapClock, Tombstone), case MapDescends of true -> Acc; - false -> dict:store(Type, {MapDots, CRDT, Tombstone}, Acc) + false -> + dict:store(Type, {Dots, CRDT, Tombstone}, Acc) end end. @@ -846,8 +876,11 @@ two_deferred_entries_test() -> {ok, {CtxB2,_,_}=StateB2} = update(?DISABLE_FLAG_B, b, StateB1, CtxC1), {ok, {CtxB3,_,_}=StateB3} = update(?REMOVE_FIELD_XA, b, StateB2, CtxB2), {ok, {_CtxB4,_,_}=StateB4} = update(?REMOVE_FIELD_XB, b, StateB3, CtxB3), - StateAB = merge(StateA1,StateB4), - ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},false}]}],value(StateAB)), + {_,Map,_}=StateAB = merge(StateA1,StateB4), + + %%Check that the element is there + {ok, {_,{_,X,_},_}} = dict:find(?FIELD,Map), + ?assertEqual(true,dict:is_key(?FIELD_B,X)), StateABC = merge(StateAB,StateC1), ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). @@ -871,17 +904,23 @@ clear_invisible_after_merge_test() -> {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_B, b, StateB1, CtxB1), {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), StateAB1 = riak_dt_map:merge(StateB3,StateA1), - riak_dt_map:value(StateAB1), StateAB2 = riak_dt_map:merge(StateB3,StateA2), - riak_dt_map:value(StateAB2). + ?assertEqual([], riak_dt_map:value(StateAB1)), + ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}]}], value(StateAB2)). + clear_invisible_after_merge_2_test() -> InitialState = riak_dt_map:new(), {ok, {CtxA1,_,_}=_StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), {ok, {_CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_B, b, InitialState,CtxA1), - {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_XYA, b, StateB1), + {ok, {CtxB2,Map,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_XYA, b, StateB1), + + %%Check that the element is there + {ok, {_,{_,X,_},_}} = dict:find(?FIELD,Map), + {ok, {_,{_,Y,_},_}} = dict:find(?FIELD_Y, X), + ?assertEqual(true,dict:is_key(?FIELD_A,Y)), {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), - ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},false}]}], value(StateB3)). + ?assertEqual([], value(StateB3)). %%Set should only have element 1. %clear_invisible_after_merge_set_test() -> From dae4417fb4b4c475ef0a930aa2e84eeed5455395 Mon Sep 17 00:00:00 2001 From: balegas Date: Fri, 9 Jan 2015 18:00:46 +0000 Subject: [PATCH 13/33] Continued with refactoring Corrected some wrong logic in clear tombstones. --- src/riak_dt_map.erl | 125 ++++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 555562a..3149afb 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -240,7 +240,7 @@ riak_dt_map:map_op(). %% @doc Create a new, empty Map. -spec new() -> map(). new() -> - {riak_dt_vclock:fresh(), dict:new(), dict:new()}. + {riak_dt_vclock:fresh(), ?DICT:new(), ?DICT:new()}. %% @doc sets the clock in the map to that `Clock'. Used by a %% containing Map for sub-CRDTs @@ -252,7 +252,7 @@ parent_clock(Clock, {_MapClock, Values, Deferred}) -> -spec value(map()) -> values(). value({_Clock, Values, _Deferred}) -> %%Selects the visible elements from the map - lists:sort(dict:fold( + lists:sort(?DICT:fold( fun({Name, Type}, {Dots, CRDT, Tombstone}, Acc) -> case Tombstone of [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; @@ -334,7 +334,7 @@ update_clock(Actor, Clock) -> {Dot, NewClock}. get({_Name, Type}=Field, Fields, Clock) -> - CRDT = case dict:find(Field, Fields) of + CRDT = case ?DICT:find(Field, Fields) of {ok, {_Dots, CRDT0, _Tombstone}} -> CRDT0; error -> @@ -343,7 +343,7 @@ get({_Name, Type}=Field, Fields, Clock) -> Type:parent_clock(Clock, CRDT). get_entry({_Name, Type}=Field, Fields, Clock) -> - {Dots, CRDT, Tombstone} = case dict:find(Field, Fields) of + {Dots, CRDT, Tombstone} = case ?DICT:find(Field, Fields) of {ok, Entry} -> Entry; error -> @@ -362,11 +362,11 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer case Type:update(Op, Dot, CRDT, Ctx) of {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), - NewValues = case dict:find(Field,Values) of + NewValues = case ?DICT:find(Field,Values) of {ok, {_,_,Tombstone}} -> - dict:store(Field, {[Dot], Updated, Tombstone}, Values); + ?DICT:store(Field, {[Dot], Updated, Tombstone}, Values); error -> - dict:store(Field, {[Dot], Updated, riak_dt_vclock:fresh()}, Values) + ?DICT:store(Field, {[Dot], Updated, riak_dt_vclock:fresh()}, Values) end, apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); Error -> @@ -395,11 +395,11 @@ apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> -spec remove_field(field(), map(), context()) -> {ok, map()} | precondition_error(). remove_field(Field, {Clock, Values, Deferred}, undefined) -> - case dict:find(Field, Values) of + case ?DICT:find(Field, Values) of error -> {error, {precondition, {not_present, Field}}}; {ok, _Removed} -> - {ok, {Clock, dict:erase(Field, Values), Deferred}} + {ok, {Clock, ?DICT:erase(Field, Values), Deferred}} end; remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> @@ -408,14 +408,14 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> NewValues = case ctx_rem_field(Field, UpdtValues, Ctx, Clock) of %Element is removed but has deferred operations empty when DefCtx =/= no_deferred -> - dict:update(Field, fun({Dots, CRDT, Tombstone}) -> + ?DICT:update(Field, fun({Dots, CRDT, Tombstone}) -> Tombstone = riak_dt_vclock:merge([DefCtx,Tombstone]), {Dots, CRDT, Tombstone} end,UpdtValues); empty -> - dict:erase(Field, UpdtValues); + ?DICT:erase(Field, UpdtValues); CRDT -> - dict:store(Field, CRDT, UpdtValues) + ?DICT:store(Field, CRDT, UpdtValues) end, {ok, {Clock, NewValues, Deferred}}. @@ -445,53 +445,62 @@ ctx_rem_field({_, Type}, {ok, {Dots, CRDT, Tombstone}}, Ctx, MapClock) -> {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2), Tombstone} end; ctx_rem_field(Field, Values, Ctx, MapClock) -> - ctx_rem_field(Field, dict:find(Field, Values), Ctx, MapClock). + ctx_rem_field(Field, ?DICT:find(Field, Values), Ctx, MapClock). %% Value is a map: %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. -propagate_remove({_, riak_dt_map}, {Dots, {ValueClock, Value0, ValueDef}, Tombstone}, MapClock, Ctx)-> +propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = - dict:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> + ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> case propagate_remove(K, V, MapClock, Ctx) of - {_,empty} -> + {_, empty} -> {UpdtClock, UpdtEntries}; {TombstoneClock, Value} -> %%Some deferred operation in subtree %% - keep entry, update tombstones {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), - dict:store(K, Value, UpdtEntries)} + ?DICT:store(K, Value, UpdtEntries)} end - end, {riak_dt_vclock:fresh(),dict:new()},Value0), + end, {riak_dt_vclock:fresh(),?DICT:new()},Value0), %Clear map if all entries are empty - case dict:size(SubEntries) of + case ?DICT:size(SubEntries) of 0 -> {SubMergedDef,empty}; - _ -> {SubMergedDef, {Dots, {ValueClock, SubEntries, ValueDef}, + _ -> {SubMergedDef, {Dots, {Clock, SubEntries, Deferred}, riak_dt_vclock:merge([SubMergedDef | Tombstone])}} end; %% Value is a leaf: -%% Merge deferred operations' context with MapClock and send it upstream -propagate_remove(Field, {Clock, {_ValueClock,_Value,ValueDef}=CRDT,TombstoneIn}, MapClock, Ctx) -> +%% Merge deferred operations' context with Value clock (Tombstone) and send it upstream +%% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? +%% Probably that make it work +propagate_remove(Field, {Clock, {_, _, Deferred}=CRDT, TombstoneIn}, MapClock, Ctx) -> %%TODO: What to do when value def is not a list? Execute the deferred op? - case ValueDef of + + %%TODO: Apply remove for fields that are tombstoned: + %%CRDT has deferred {a,1}, a posterior 'add' operation {b,1}. + %%Should remove {b,1} and create the tombstone. Now, it does + %%not remove {b,1}, so if the CRDT is not removed afterall, because there + %%was a concurrent operation, element with dot {b,1} will be present though + %%it was removed. + case Deferred of [] -> {[], ctx_rem_field(Field, {ok, {Clock, CRDT, []}}, Ctx, MapClock)}; _ -> Intersection = riak_dt_vclock:glb(Clock,Ctx), - Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | ValueDef]), + Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Deferred]), {Tombstone, {Clock, CRDT, Tombstone}} end; propagate_remove(Field, Values, MapClock, Ctx) -> - case dict:find(Field, Values) of + case ?DICT:find(Field, Values) of {ok,Value} -> {UpdtClock,UpdtValue} = propagate_remove(Field, Value, MapClock, Ctx), case UpdtClock of MapClock -> {no_deferred, Values}; - _ -> {UpdtClock, dict:store(Field, UpdtValue, Values)} + _ -> {UpdtClock, ?DICT:store(Field, UpdtValue, Values)} end; error -> {MapClock, Values} end. @@ -515,7 +524,7 @@ defer_remove(Clock, Ctx, Field, Deferred) -> case riak_dt_vclock:descends(Clock, Ctx) of %% no need to save this remove, we're done true -> Deferred; - false -> dict:update(Ctx, + false -> ?DICT:update(Ctx, fun(Fields) -> ordsets:add_element(Field, Fields) end, ordsets:add_element(Field, ordsets:new()), @@ -524,7 +533,7 @@ defer_remove(Clock, Ctx, Field, Deferred) -> merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) -> Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), - Fields = lists:umerge(dict:fetch_keys(LHSEntries), dict:fetch_keys(RHSEntries)), + Fields = lists:umerge(?DICT:fetch_keys(LHSEntries), ?DICT:fetch_keys(RHSEntries)), Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> {LHSDots, LHSCRDT, LHDTomb} = get_entry(Field, LHSEntries, LHSClock), {RHSDots, RHSCRDT, RHDTomb} = get_entry(Field, RHSEntries, RHSClock), @@ -535,10 +544,10 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) CRDT = Type:merge(LHSCRDT, RHSCRDT), MergedTombstone = riak_dt_vclock:merge([LHDTomb,RHDTomb]), %% Yes! Reset the clock, again - dict:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT), MergedTombstone}, Acc) + ?DICT:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT), MergedTombstone}, Acc) end end, - dict:new(), + ?DICT:new(), Fields), Deferred = merge_deferred(LHSDeferred, RHSDeferred), CRDT = apply_deferred(Clock, Entries, Deferred), @@ -556,7 +565,7 @@ keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> %% @private -spec merge_deferred(deferred(), deferred()) -> deferred(). merge_deferred(LHS, RHS) -> - dict:merge(fun(_K, LH, RH) -> + ?DICT:merge(fun(_K, LH, RH) -> ordsets:union(LH, RH) end, LHS, RHS). @@ -585,35 +594,35 @@ remove_all(Fields, Map, Ctx) -> %Eliminates the tombstone if it has been integrated in the object's clock. clear_tombstones({Clock, Entries, Deferred}) -> FilteredEntries = - ?DICT:fold(fun(Type, Value, Acc) -> - clear_tombstones_handle(Type, Value, Acc, Clock) - end, dict:new(), Entries), + ?DICT:fold(fun(Field_i, Value_i, FilteredEntriesAcc) -> + clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, Clock) + end, ?DICT:new(), Entries), {Clock, FilteredEntries, Deferred}. -clear_tombstones_handle({_, riak_dt_map}=Field, {Dots, {Clock, CRDT, Deferred}, Tombstone}, Acc, MapClock) -> +clear_tombstones_handle({_, riak_dt_map}=Field, {Dots, {Clock, CRDT, Deferred}, Tombstone}, NewMap, MapClock) -> FilteredEntries = - ?DICT:fold(fun(Type, Value, AccMap) -> - clear_tombstones_handle(Type, Value, AccMap, MapClock) - end, dict:new(), CRDT), - case dict:size(FilteredEntries) == 0 of - true when length(Tombstone) > 0 -> Acc; - _ -> dict:store(Field, {Dots, {Clock, FilteredEntries, Deferred}, Tombstone}, Acc) + ?DICT:fold(fun(Field_i, Value_i, FilteredEntriesAcc) -> + clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, MapClock) + end, ?DICT:new(), CRDT), + %%Distinguish between empty map and removed map. + case ?DICT:size(FilteredEntries) == 0 of + true when length(Tombstone) > 0 -> NewMap; + _ -> ?DICT:store(Field, {Dots, {Clock, FilteredEntries, Deferred}, Tombstone}, NewMap) end; -clear_tombstones_handle(Type, {Dots, CRDT, Tombstone}, Acc, MapClock) -> - ObjDominatesTomb = riak_dt_vclock:dominates(Dots, Tombstone), - case ObjDominatesTomb of +clear_tombstones_handle(Field, {Dots, CRDT, Tombstone}=Value, NewMap, MapClock) -> + TombstoneCovered = riak_dt_vclock:descends(MapClock, Tombstone), + case TombstoneCovered of true -> - %Object received concurrent update - dict:store(Type, {Dots, CRDT, []}, Acc); - false -> - MapDescends = riak_dt_vclock:descends(MapClock, Tombstone), - case MapDescends of + ReceivedUpdates = riak_dt_vclock:dominates(Dots, Tombstone), + case ReceivedUpdates of true -> - Acc; - false -> - dict:store(Type, {Dots, CRDT, Tombstone}, Acc) - end + ?DICT:store(Field, {Dots, CRDT, []}, NewMap); + false -> + NewMap + end; + false -> + ?DICT:store(Field, Value, NewMap) end. %% @doc compare two `map()'s for equality of structure Both schemas @@ -879,8 +888,8 @@ two_deferred_entries_test() -> {_,Map,_}=StateAB = merge(StateA1,StateB4), %%Check that the element is there - {ok, {_,{_,X,_},_}} = dict:find(?FIELD,Map), - ?assertEqual(true,dict:is_key(?FIELD_B,X)), + {ok, {_,{_,X,_},_}} = ?DICT:find(?FIELD,Map), + ?assertEqual(true,?DICT:is_key(?FIELD_B,X)), StateABC = merge(StateAB,StateC1), ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). @@ -916,9 +925,9 @@ clear_invisible_after_merge_2_test() -> {ok, {CtxB2,Map,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_XYA, b, StateB1), %%Check that the element is there - {ok, {_,{_,X,_},_}} = dict:find(?FIELD,Map), - {ok, {_,{_,Y,_},_}} = dict:find(?FIELD_Y, X), - ?assertEqual(true,dict:is_key(?FIELD_A,Y)), + {ok, {_,{_,X,_},_}} = ?DICT:find(?FIELD,Map), + {ok, {_,{_,Y,_},_}} = ?DICT:find(?FIELD_Y, X), + ?assertEqual(true,?DICT:is_key(?FIELD_A,Y)), {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), ?assertEqual([], value(StateB3)). From f28af3420907516a7fd89016c2b911cb3c0427d7 Mon Sep 17 00:00:00 2001 From: balegas Date: Mon, 12 Jan 2015 17:07:06 +0000 Subject: [PATCH 14/33] Added get_deferred operation. --- src/riak_dt.erl | 16 +++++++++++++++- src/riak_dt_disable_flag.erl | 8 +++++++- src/riak_dt_emcntr.erl | 6 +++++- src/riak_dt_enable_flag.erl | 6 +++++- src/riak_dt_gcounter.erl | 6 +++++- src/riak_dt_gset.erl | 6 +++++- src/riak_dt_lwwreg.erl | 6 +++++- src/riak_dt_map.erl | 10 ++++++++-- src/riak_dt_od_flag.erl | 8 +++++++- src/riak_dt_oe_flag.erl | 6 +++++- src/riak_dt_orset.erl | 6 +++++- src/riak_dt_orswot.erl | 8 +++++++- src/riak_dt_pncounter.erl | 6 +++++- 13 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/riak_dt.erl b/src/riak_dt.erl index 2a13699..3a3c364 100644 --- a/src/riak_dt.erl +++ b/src/riak_dt.erl @@ -22,7 +22,7 @@ -module(riak_dt). --export([to_binary/1, from_binary/1, dict_to_orddict/1]). +-export([get_deferred/1, get_deferred/2, to_binary/1, from_binary/1, dict_to_orddict/1]). -export_type([actor/0, dot/0, crdt/0, context/0]). -type crdt() :: term(). @@ -44,6 +44,7 @@ %% given clock as it's own. -callback parent_clock(riak_dt_vclock:vclock(), crdt()) -> crdt(). +-callback get_deferred(crdt(), riak_dt_vclock:vclock()) -> [riak_dt_vclock:vclock()]. -callback merge(crdt(), crdt()) -> crdt(). -callback equal(crdt(), crdt()) -> boolean(). -callback to_binary(crdt()) -> binary(). @@ -81,3 +82,16 @@ from_binary(Binary) -> -spec dict_to_orddict(dict()) -> orddict:orddict(). dict_to_orddict(Dict) -> orddict:from_list(dict:to_list(Dict)). + +-spec get_deferred(crdt()) -> [riak_dt_vclock:vclock()]. +get_deferred({_, _, Deferred}) -> Deferred. + +-spec get_deferred(crdt(), riak_dt_vclock:vclock()) -> [riak_dt_vclock:vclock()]. +get_deferred({_, _, Deferred}, Ctx) -> + lists:filtermap(fun(Def) -> + case riak_dt_vclock:glb(Def,Ctx) of + [] -> false; + Intersect -> {true, Intersect} + end + end, Deferred). + diff --git a/src/riak_dt_disable_flag.erl b/src/riak_dt_disable_flag.erl index 6447185..d11def6 100644 --- a/src/riak_dt_disable_flag.erl +++ b/src/riak_dt_disable_flag.erl @@ -30,7 +30,7 @@ -behaviour(riak_dt). -export([new/0, value/1, value/2, update/3, merge/2, equal/2, from_binary/1, to_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). -include_lib("eqc/include/eqc.hrl"). @@ -82,6 +82,12 @@ equal(FA,FB) -> parent_clock(_Clock, Flag) -> Flag. +get_deferred(CRDT) -> + riak_dt:get_deferred(CRDT). + +get_deferred(CRDT, Ctx) -> + riak_dt:get_deferred(CRDT, Ctx). + -spec from_binary(binary()) -> disable_flag(). from_binary(<>) -> off; from_binary(<>) -> on. diff --git a/src/riak_dt_emcntr.erl b/src/riak_dt_emcntr.erl index f559b8a..4a3119b 100644 --- a/src/riak_dt_emcntr.erl +++ b/src/riak_dt_emcntr.erl @@ -50,7 +50,7 @@ -export([to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([stats/1, stat/2]). --export([parent_clock/2, update/4]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). %% EQC API -ifdef(EQC). @@ -85,6 +85,10 @@ new() -> parent_clock(Clock, {_, Cntr}) -> {Clock, Cntr}. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + %% @doc the current integer value of the counter -spec value(emcntr()) -> integer(). value({_Clock, PNCnt}) -> diff --git a/src/riak_dt_enable_flag.erl b/src/riak_dt_enable_flag.erl index 29a41dc..c0d59b9 100644 --- a/src/riak_dt_enable_flag.erl +++ b/src/riak_dt_enable_flag.erl @@ -30,7 +30,7 @@ -behaviour(riak_dt). -export([new/0, value/1, value/2, update/3, merge/2, equal/2, from_binary/1, to_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -74,6 +74,10 @@ update(enable, _Actor, _Flag, _Ctx) -> parent_clock(_Clock, Flag) -> Flag. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + -spec merge(enable_flag(), enable_flag()) -> enable_flag(). merge(FA, FB) -> flag_or(FA, FB). diff --git a/src/riak_dt_gcounter.erl b/src/riak_dt_gcounter.erl index 544ddd9..926d644 100644 --- a/src/riak_dt_gcounter.erl +++ b/src/riak_dt_gcounter.erl @@ -37,7 +37,7 @@ -module(riak_dt_gcounter). -behaviour(riak_dt). -export([new/0, new/2, value/1, value/2, update/3, merge/2, equal/2, to_binary/1, from_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). %% EQC API @@ -95,6 +95,10 @@ update(Op, Actor, GCnt, _Ctx) -> parent_clock(_Clock, GCnt) -> GCnt. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + %% @doc Merge two `gcounter()'s to a single `gcounter()'. This is the Least Upper Bound %% function described in the literature. -spec merge(gcounter(), gcounter()) -> gcounter(). diff --git a/src/riak_dt_gset.erl b/src/riak_dt_gset.erl index ee79d7f..faf3520 100644 --- a/src/riak_dt_gset.erl +++ b/src/riak_dt_gset.erl @@ -35,7 +35,7 @@ %% API -export([new/0, value/1, update/3, merge/2, equal/2, to_binary/1, from_binary/1, value/2, stats/1, stat/2]). --export([update/4, parent_clock/2]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -92,6 +92,10 @@ update(Op, Actor, GSet, _Ctx) -> parent_clock(_Clock, GSet) -> GSet. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + -spec merge(gset(), gset()) -> gset(). merge(GSet1, GSet2) -> ordsets:union(GSet1, GSet2). diff --git a/src/riak_dt_lwwreg.erl b/src/riak_dt_lwwreg.erl index 4f7c470..d2123aa 100644 --- a/src/riak_dt_lwwreg.erl +++ b/src/riak_dt_lwwreg.erl @@ -33,7 +33,7 @@ -export([new/0, value/1, value/2, update/3, merge/2, equal/2, to_binary/1, from_binary/1, stats/1, stat/2]). --export([parent_clock/2, update/4]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). %% EQC API @@ -63,6 +63,10 @@ new() -> parent_clock(_Clock, Reg) -> Reg. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + %% @doc The single total value of a `gcounter()'. -spec value(lwwreg()) -> term(). value({Value, _TS}) -> diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 3149afb..57ebb2a 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -180,7 +180,7 @@ -export([merge/2, equal/2, to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1, stats/1, stat/2]). --export([parent_clock/2]). +-export([parent_clock/2, get_deferred/1, get_deferred/2]). %% EQC API -ifdef(EQC). @@ -248,6 +248,12 @@ new() -> parent_clock(Clock, {_MapClock, Values, Deferred}) -> {Clock, Values, Deferred}. +get_deferred({_, _, Deferred}) -> + lists:map(fun({Key, _}) -> Key end, ?DICT:to_list(Deferred)). + +get_deferred({Clock, Entries, _}=CRDT, Ctx) -> + riak_dt:get_deferred({Clock, Entries, get_deferred(CRDT)}, Ctx). + %% @doc get the current set of values for this Map -spec value(map()) -> values(). value({_Clock, Values, _Deferred}) -> @@ -475,7 +481,7 @@ propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, %% Value is a leaf: %% Merge deferred operations' context with Value clock (Tombstone) and send it upstream %% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? -%% Probably that make it work +%% Only handles half of the problem. propagate_remove(Field, {Clock, {_, _, Deferred}=CRDT, TombstoneIn}, MapClock, Ctx) -> %%TODO: What to do when value def is not a list? Execute the deferred op? diff --git a/src/riak_dt_od_flag.erl b/src/riak_dt_od_flag.erl index 327d007..6560b64 100644 --- a/src/riak_dt_od_flag.erl +++ b/src/riak_dt_od_flag.erl @@ -30,7 +30,7 @@ -export([to_binary/1, stats/1, stat/2]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1]). --export([parent_clock/2]). +-export([parent_clock/2, get_deferred/1, get_deferred/2]). -ifdef(EQC). -include_lib("eqc/include/eqc.hrl"). @@ -58,6 +58,12 @@ new() -> parent_clock(Clock, {_SetClock, Flag , Deferred}) -> {Clock, Flag, Deferred}. +get_deferred(CRDT) -> + riak_dt:get_deferred(CRDT). + +get_deferred(CRDT, Ctx) -> + riak_dt:get_deferred(CRDT, Ctx). + -spec value(od_flag()) -> boolean(). value({_, [], _}) -> false; value({_, _, _}) -> true. diff --git a/src/riak_dt_oe_flag.erl b/src/riak_dt_oe_flag.erl index 50ad0cf..20c8562 100644 --- a/src/riak_dt_oe_flag.erl +++ b/src/riak_dt_oe_flag.erl @@ -26,7 +26,7 @@ -behaviour(riak_dt). -export([new/0, value/1, value/2, update/3, merge/2, equal/2, from_binary/1, to_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -72,6 +72,10 @@ update(Op, Actor, Flag, _Ctx) -> parent_clock(_Clock, Flag) -> Flag. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + -spec merge(oe_flag(), oe_flag()) -> oe_flag(). merge({C1, F}, {C2,F}) -> %% When they are the same result (true or false), just merge the diff --git a/src/riak_dt_orset.erl b/src/riak_dt_orset.erl index 4a4afa2..f49c84c 100644 --- a/src/riak_dt_orset.erl +++ b/src/riak_dt_orset.erl @@ -27,7 +27,7 @@ %% API -export([new/0, value/1, update/3, merge/2, equal/2, to_binary/1, from_binary/1, value/2, precondition_context/1, stats/1, stat/2]). --export([update/4, parent_clock/2]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -118,6 +118,10 @@ update(Op, Actor, ORDict, _Ctx) -> parent_clock(_Clock, ORSet) -> ORSet. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + -spec merge(orset(), orset()) -> orset(). merge(ORDictA, ORDictB) -> orddict:merge(fun(_Elem,TokensA,TokensB) -> diff --git a/src/riak_dt_orswot.erl b/src/riak_dt_orswot.erl index 006dbc4..25e12a1 100644 --- a/src/riak_dt_orswot.erl +++ b/src/riak_dt_orswot.erl @@ -80,7 +80,7 @@ -export([to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1, stats/1, stat/2]). --export([parent_clock/2]). +-export([parent_clock/2, get_deferred/1, get_deferred/2]). %% EQC API -ifdef(EQC). @@ -132,6 +132,12 @@ new() -> parent_clock(Clock, {_SetClock, Entries, Deferred}) -> {Clock, Entries, Deferred}. +get_deferred({_, _, Deferred}) -> + lists:map(fun({Key, _}) -> Key end, ?DICT:to_list(Deferred)). + +get_deferred({Clock, Entries, _}=CRDT, Ctx) -> + riak_dt:get_deferred({Clock, Entries, get_deferred(CRDT)}, Ctx). + -spec value(orswot()) -> [member()]. value({_Clock, Entries, _Deferred}) -> lists:sort([K || {K, _Dots} <- ?DICT:to_list(Entries)]). diff --git a/src/riak_dt_pncounter.erl b/src/riak_dt_pncounter.erl index cf5ffb5..fb72ef4 100644 --- a/src/riak_dt_pncounter.erl +++ b/src/riak_dt_pncounter.erl @@ -38,7 +38,7 @@ -export([new/0, new/2, value/1, value/2, update/3, merge/2, equal/2, to_binary/1, from_binary/1, stats/1, stat/2]). -export([to_binary/2, from_binary/2, current_version/1, change_versions/3]). --export([parent_clock/2, update/4]). +-export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). %% EQC API -ifdef(EQC). @@ -81,6 +81,10 @@ new(_Actor, _Zero) -> parent_clock(_Clock, Cntr) -> Cntr. +get_deferred(_CRDT) -> []. + +get_deferred(_CRDT, _Ctx) -> []. + %% @doc The single, total value of a `pncounter()' -spec value(pncounter()) -> integer(). value(PNCnt) -> From 093689a743eccebdde198c95e458c98cca9c90df Mon Sep 17 00:00:00 2001 From: balegas Date: Mon, 12 Jan 2015 17:39:30 +0000 Subject: [PATCH 15/33] Fixed issue with reading from non-lists deferred operations. Added new failing test. --- src/riak_dt.erl | 1 + src/riak_dt_map.erl | 50 +++++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/riak_dt.erl b/src/riak_dt.erl index 3a3c364..a95055a 100644 --- a/src/riak_dt.erl +++ b/src/riak_dt.erl @@ -44,6 +44,7 @@ %% given clock as it's own. -callback parent_clock(riak_dt_vclock:vclock(), crdt()) -> crdt(). +-callback get_deferred(crdt()) -> [riak_dt_vclock:vclock()]. -callback get_deferred(crdt(), riak_dt_vclock:vclock()) -> [riak_dt_vclock:vclock()]. -callback merge(crdt(), crdt()) -> crdt(). -callback equal(crdt(), crdt()) -> boolean(). diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 57ebb2a..cf39d72 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -482,21 +482,19 @@ propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, %% Merge deferred operations' context with Value clock (Tombstone) and send it upstream %% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? %% Only handles half of the problem. -propagate_remove(Field, {Clock, {_, _, Deferred}=CRDT, TombstoneIn}, MapClock, Ctx) -> - %%TODO: What to do when value def is not a list? Execute the deferred op? - +propagate_remove({_, Type}=Field, {Clock, CRDT, TombstoneIn}, MapClock, Ctx) -> %%TODO: Apply remove for fields that are tombstoned: %%CRDT has deferred {a,1}, a posterior 'add' operation {b,1}. %%Should remove {b,1} and create the tombstone. Now, it does %%not remove {b,1}, so if the CRDT is not removed afterall, because there %%was a concurrent operation, element with dot {b,1} will be present though %%it was removed. - case Deferred of + case Type:get_deferred(CRDT) of [] -> {[], ctx_rem_field(Field, {ok, {Clock, CRDT, []}}, Ctx, MapClock)}; _ -> Intersection = riak_dt_vclock:glb(Clock,Ctx), - Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Deferred]), + Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Type:get_deferred(CRDT)]), {Tombstone, {Clock, CRDT, Tombstone}} end; @@ -776,6 +774,7 @@ from_binary(?V2_VERS, <>) -> -define(FIELD_X, {'X', riak_dt_od_flag}). -define(FIELD_A, {'X.A', riak_dt_od_flag}). -define(FIELD_B, {'X.B', riak_dt_od_flag}). +-define(FIELD_S, {'X.S', riak_dt_orswot}). -define(ENABLE_FLAG_A, {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}]}}]}). -define(ENABLE_FLAG_B, {update, [{update, ?FIELD, {update, [{update, ?FIELD_B, enable}]}}]}). @@ -937,19 +936,34 @@ clear_invisible_after_merge_2_test() -> {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), ?assertEqual([], value(StateB3)). -%%Set should only have element 1. -%clear_invisible_after_merge_set_test() -> -% AddElemToB = fun(Elem) -> -% {update, [{update, ?FIELD, {update, [{update, ?FIELD_B, {add, Elem}}]}}]} -% end, -% InitialState = riak_dt_map:new(), -% {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), -% {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_A, b, InitialState, CtxA1), -% {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(AddElemToB(0), b, StateB1, CtxB1), -% {ok, {CtxB3,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), -% {ok, {CtxB4,_,_}=StateB4} = riak_dt_map:update(AddElemToB(1), b, StateB3, CtxB3), - -%%TODO: Visibility test; Remove with a context that does not cover a deferred operation; +clear_invisible_after_merge_set_test() -> + AddElemToS = fun(Elem) -> + {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {add, Elem}}]}}]} + end, + InitialState = riak_dt_map:new(), + {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(AddElemToS(0), b, StateB1, CtxB1), + {ok, {CtxB3,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + {ok, {_,_,_}=StateB4} = riak_dt_map:update(AddElemToS(1), b, StateB3, CtxB3), + StateAB = riak_dt_map:merge(StateA1,StateB4), + ?assertEqual([{{'X',riak_dt_map}, [{{'X.S',riak_dt_orswot},[1]}]}], value(StateAB)). + +clear_invisible_after_merge_set_2_test() -> + AddElemToS = fun(Elem) -> + {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {add, Elem}}]}}]} + end, + RemElemFromS = fun(Elem) -> + {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {remove, Elem}}]}}]} + end, + InitialState = riak_dt_map:new(), + {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(AddElemToS(0), a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(RemElemFromS(0), b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(AddElemToS(1), b, StateB1, CtxB1), + {ok, {CtxB3,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + {ok, {_,_,_}=StateB4} = riak_dt_map:update(AddElemToS(2), b, StateB3, CtxB3), + StateAB = riak_dt_map:merge(StateA1,StateB4), + ?assertEqual([{{'X',riak_dt_map}, [{{'X.S',riak_dt_orswot},[2]}]}], value(StateAB)). %% This fails on previous version of riak_dt_map assoc_test() -> From ff3f73c96128fa28c8dc7c1dc45105e3e31d11a5 Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 13 Jan 2015 00:00:05 +0000 Subject: [PATCH 16/33] Added clear operations to some CRDTs. Use it on propagate_remove() to fix error in test. --- src/riak_dt_map.erl | 3 ++- src/riak_dt_od_flag.erl | 6 +++++- src/riak_dt_orswot.erl | 10 +++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index cf39d72..583c088 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -495,7 +495,8 @@ propagate_remove({_, Type}=Field, {Clock, CRDT, TombstoneIn}, MapClock, Ctx) -> _ -> Intersection = riak_dt_vclock:glb(Clock,Ctx), Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Type:get_deferred(CRDT)]), - {Tombstone, {Clock, CRDT, Tombstone}} + {ok, ClearedCRDT} = Type:clear(CRDT, Ctx), + {Tombstone, {Clock, ClearedCRDT, Tombstone}} end; propagate_remove(Field, Values, MapClock, Ctx) -> diff --git a/src/riak_dt_od_flag.erl b/src/riak_dt_od_flag.erl index 6560b64..a7b8fcf 100644 --- a/src/riak_dt_od_flag.erl +++ b/src/riak_dt_od_flag.erl @@ -30,7 +30,7 @@ -export([to_binary/1, stats/1, stat/2]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1]). --export([parent_clock/2, get_deferred/1, get_deferred/2]). +-export([parent_clock/2, get_deferred/1, get_deferred/2, clear/1, clear/2]). -ifdef(EQC). -include_lib("eqc/include/eqc.hrl"). @@ -154,6 +154,10 @@ apply_deferred(Clock, Flag, Deferred) -> {Clock, Flag, []}, Deferred). +clear(Flag) -> disable(Flag, undefined). + +clear(Flag, Ctx) -> disable(Flag, Ctx). + -spec equal(od_flag(), od_flag()) -> boolean(). equal({C1,D1, Def1},{C2,D2, Def2}) -> riak_dt_vclock:equal(C1,C2) andalso diff --git a/src/riak_dt_orswot.erl b/src/riak_dt_orswot.erl index 25e12a1..1d9b377 100644 --- a/src/riak_dt_orswot.erl +++ b/src/riak_dt_orswot.erl @@ -80,7 +80,7 @@ -export([to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1, stats/1, stat/2]). --export([parent_clock/2, get_deferred/1, get_deferred/2]). +-export([parent_clock/2, get_deferred/1, get_deferred/2, clear/1, clear/2]). %% EQC API -ifdef(EQC). @@ -381,6 +381,14 @@ remove_elem({ok, _VClock}, Elem, {Clock, Dict, Deferred}) -> remove_elem(_, Elem, _ORSet) -> {error, {precondition, {not_present, Elem}}}. +clear({_, Entries, _}=ORSet) -> + Elems = ?DICT:fold(fun(Elem, _, Acc) -> [Elem | Acc] end, [], Entries), + remove_all(Elems, undefined, ORSet). + +clear({_, Entries, _}=ORSet, Ctx) -> + Elems = ?DICT:fold(fun(Elem, _, Acc) -> [Elem | Acc] end, [], Entries), + remove_all(Elems, undefined, ORSet, Ctx). + %% @doc the precondition context is a fragment of the CRDT that %% operations requiring certain pre-conditions can be applied with. %% Especially useful for hybrid op/state systems where the context of From a06dad474a737f377696e845cdd66a4c338d1d67 Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 13 Jan 2015 12:07:27 +0000 Subject: [PATCH 17/33] Simpler way to clear the CRDT and is not exposed in the interface. --- src/riak_dt_map.erl | 13 ++++++------- src/riak_dt_od_flag.erl | 1 + src/riak_dt_orswot.erl | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 583c088..73c9a6b 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -483,19 +483,18 @@ propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, %% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? %% Only handles half of the problem. propagate_remove({_, Type}=Field, {Clock, CRDT, TombstoneIn}, MapClock, Ctx) -> - %%TODO: Apply remove for fields that are tombstoned: - %%CRDT has deferred {a,1}, a posterior 'add' operation {b,1}. - %%Should remove {b,1} and create the tombstone. Now, it does - %%not remove {b,1}, so if the CRDT is not removed afterall, because there - %%was a concurrent operation, element with dot {b,1} will be present though - %%it was removed. case Type:get_deferred(CRDT) of [] -> {[], ctx_rem_field(Field, {ok, {Clock, CRDT, []}}, Ctx, MapClock)}; _ -> Intersection = riak_dt_vclock:glb(Clock,Ctx), Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Type:get_deferred(CRDT)]), - {ok, ClearedCRDT} = Type:clear(CRDT, Ctx), + + %% Clear CRDT + TombstoneClock = riak_dt_vclock:glb(MapClock, Ctx), + TS = Type:parent_clock(TombstoneClock, Type:new()), + ClearedCRDT = Type:merge(TS, Type:parent_clock(TombstoneClock, CRDT)), + {Tombstone, {Clock, ClearedCRDT, Tombstone}} end; diff --git a/src/riak_dt_od_flag.erl b/src/riak_dt_od_flag.erl index a7b8fcf..67dcad2 100644 --- a/src/riak_dt_od_flag.erl +++ b/src/riak_dt_od_flag.erl @@ -154,6 +154,7 @@ apply_deferred(Clock, Flag, Deferred) -> {Clock, Flag, []}, Deferred). +%No longer needed clear(Flag) -> disable(Flag, undefined). clear(Flag, Ctx) -> disable(Flag, Ctx). diff --git a/src/riak_dt_orswot.erl b/src/riak_dt_orswot.erl index 1d9b377..1968a4b 100644 --- a/src/riak_dt_orswot.erl +++ b/src/riak_dt_orswot.erl @@ -381,6 +381,7 @@ remove_elem({ok, _VClock}, Elem, {Clock, Dict, Deferred}) -> remove_elem(_, Elem, _ORSet) -> {error, {precondition, {not_present, Elem}}}. +%No longer needed clear({_, Entries, _}=ORSet) -> Elems = ?DICT:fold(fun(Elem, _, Acc) -> [Elem | Acc] end, [], Entries), remove_all(Elems, undefined, ORSet). From f48ea2bc5cc5ff5be42d08fc7f56d9892d50e5e1 Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 13 Jan 2015 15:23:31 +0000 Subject: [PATCH 18/33] Removed redundant ctx_rem_field operation. --- src/riak_dt_map.erl | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 73c9a6b..d83578e 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -411,17 +411,19 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), {DefCtx, UpdtValues} = propagate_remove(Field, Values, Clock, Ctx), - NewValues = case ctx_rem_field(Field, UpdtValues, Ctx, Clock) of + NewValues = case ?DICT:find(Field, UpdtValues) of %Element is removed but has deferred operations - empty when DefCtx =/= no_deferred -> + {ok, empty} when DefCtx =/= no_deferred -> ?DICT:update(Field, fun({Dots, CRDT, Tombstone}) -> Tombstone = riak_dt_vclock:merge([DefCtx,Tombstone]), {Dots, CRDT, Tombstone} end,UpdtValues); - empty -> + {ok, empty} -> ?DICT:erase(Field, UpdtValues); - CRDT -> - ?DICT:store(Field, CRDT, UpdtValues) + {ok, CRDT} -> + ?DICT:store(Field, CRDT, UpdtValues); + error -> + UpdtValues end, {ok, {Clock, NewValues, Deferred}}. @@ -502,9 +504,11 @@ propagate_remove(Field, Values, MapClock, Ctx) -> case ?DICT:find(Field, Values) of {ok,Value} -> {UpdtClock,UpdtValue} = propagate_remove(Field, Value, MapClock, Ctx), - case UpdtClock of - MapClock -> {no_deferred, Values}; - _ -> {UpdtClock, ?DICT:store(Field, UpdtValue, Values)} + case UpdtValue of + empty -> + {UpdtClock, ?DICT:erase(Field, Values)}; + _ -> + {UpdtClock, ?DICT:store(Field, UpdtValue, Values)} end; error -> {MapClock, Values} end. From a594530c9e5f70e40c3331648d7a5b610b805020 Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 13 Jan 2015 15:32:32 +0000 Subject: [PATCH 19/33] Added transactional tests --- src/riak_dt_map.erl | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index d83578e..77f4b65 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -415,9 +415,9 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> %Element is removed but has deferred operations {ok, empty} when DefCtx =/= no_deferred -> ?DICT:update(Field, fun({Dots, CRDT, Tombstone}) -> - Tombstone = riak_dt_vclock:merge([DefCtx,Tombstone]), - {Dots, CRDT, Tombstone} - end,UpdtValues); + Tombstone = riak_dt_vclock:merge([DefCtx,Tombstone]), + {Dots, CRDT, Tombstone} + end,UpdtValues); {ok, empty} -> ?DICT:erase(Field, UpdtValues); {ok, CRDT} -> @@ -474,7 +474,7 @@ propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, %Clear map if all entries are empty case ?DICT:size(SubEntries) of 0 -> - {SubMergedDef,empty}; + {SubMergedDef, empty}; _ -> {SubMergedDef, {Dots, {Clock, SubEntries, Deferred}, riak_dt_vclock:merge([SubMergedDef | Tombstone])}} end; @@ -926,7 +926,6 @@ clear_invisible_after_merge_test() -> ?assertEqual([], riak_dt_map:value(StateAB1)), ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}]}], value(StateAB2)). - clear_invisible_after_merge_2_test() -> InitialState = riak_dt_map:new(), {ok, {CtxA1,_,_}=_StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), @@ -969,6 +968,28 @@ clear_invisible_after_merge_set_2_test() -> StateAB = riak_dt_map:merge(StateA1,StateB4), ?assertEqual([{{'X',riak_dt_map}, [{{'X.S',riak_dt_orswot},[2]}]}], value(StateAB)). +transaction_1_test() -> + Updt1 = {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}, {update, ?FIELD_B, enable}]}}]}, + InitialState = riak_dt_map:new(), + {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(Updt1, a, InitialState), + ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, + {{'X.B',riak_dt_od_flag},true}]}], value(StateA1)), + {ok, StateB1} = riak_dt_map:update(?REMOVE_FIELD_X, b, InitialState, CtxA1), + StateAB = riak_dt_map:merge(StateA1,StateB1), + ?assertEqual([], riak_dt_map:value(StateAB)). + +transaction_2_test() -> + Updt1 = {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}, {update, ?FIELD_B, enable}]}}]}, + InitialState = riak_dt_map:new(), + {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(Updt1, a, InitialState), + {ok, StateB1} = riak_dt_map:update(?REMOVE_FIELD_XA, b, InitialState, CtxA1), + StateAB = riak_dt_map:merge(StateA1,StateB1), + ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},true}]}], value(StateAB)), + {ok, StateA2} = riak_dt_map:update(?ENABLE_FLAG_A, a, StateA1), + StateA2B = riak_dt_map:merge(StateA2,StateB1), + ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, + {{'X.B',riak_dt_od_flag},true}]}], value(StateA2B)). + %% This fails on previous version of riak_dt_map assoc_test() -> Field = {'X', riak_dt_orswot}, From 35874372756bfe95509e40a296c8a89da9cfa5ad Mon Sep 17 00:00:00 2001 From: balegas Date: Thu, 22 Jan 2015 23:07:56 +0000 Subject: [PATCH 20/33] Separate meta-data from value --- src/riak_dt_map.erl | 172 +++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 83 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 77f4b65..602113a 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -192,9 +192,10 @@ -type binary_map() :: binary(). %% A binary that from_binary/1 will accept -type map() :: {riak_dt_vclock:vclock(), entries(), deferred()}. -type entries() :: [field()]. --type field() :: {field_name(), field_value()}. +-type field() :: {field_name(), {field_meta(), field_value()}}. -type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. --type field_value() :: {riak_dt_vclock:vclock(), crdt(), riak_dt_vclock:vclock()}. +-type field_meta() :: {riak_dt_vclock:vclock(), seen(), tombstone()}. +-type field_value() :: crdt(). %%-type crdts() :: [entry()]. %%-type entry() :: {riak_dt:dot(), crdt()}. @@ -205,7 +206,12 @@ %% Only field removals can be deferred. CRDTs stored in the map may %% have contexts and deferred operations, but as these are part of the %% state, they are stored under the field as an update like any other. +-type seen() :: dots(). -type deferred() :: [{context(), [field()]}]. +-type tombstone() :: riak_dt_vclock:vclock(). + +-type dots() :: [dot()]. +-type dot() :: riak_dt:dot(). %% limited to only those mods that support both a shared causal %% context, and by extension, the reset-remove semantic. @@ -216,7 +222,7 @@ riak_dt_map | riak_dt_orswot. -type crdt() :: riak_dt_emcntr:emcntr() | riak_dt_od_flag:od_flag() | riak_dt_lwwreg:lwwreg() | riak_dt_orswot:orswot() | -riak_dt_map:map(). +riak_dt_map:map() | riak_dt_delta_map:map(). -type map_op() :: {update, [map_field_update() | map_field_op()]}. @@ -226,7 +232,7 @@ riak_dt_map:map(). -type crdt_op() :: riak_dt_emcntr:emcntr_op() | riak_dt_lwwreg:lwwreg_op() | riak_dt_orswot:orswot_op() | riak_dt_od_flag:od_flag_op() | -riak_dt_map:map_op(). +riak_dt_map:map_op() | riak_dt_map:map_op(). -type context() :: riak_dt_vclock:vclock() | undefined. @@ -259,7 +265,7 @@ get_deferred({Clock, Entries, _}=CRDT, Ctx) -> value({_Clock, Values, _Deferred}) -> %%Selects the visible elements from the map lists:sort(?DICT:fold( - fun({Name, Type}, {Dots, CRDT, Tombstone}, Acc) -> + fun({Name, Type}, {{Dots, _, Tombstone}, CRDT}, Acc) -> case Tombstone of [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; _ -> @@ -341,7 +347,7 @@ update_clock(Actor, Clock) -> get({_Name, Type}=Field, Fields, Clock) -> CRDT = case ?DICT:find(Field, Fields) of - {ok, {_Dots, CRDT0, _Tombstone}} -> + {ok, {{_Dots, _, _},CRDT0}} -> CRDT0; error -> Type:new() @@ -349,13 +355,13 @@ get({_Name, Type}=Field, Fields, Clock) -> Type:parent_clock(Clock, CRDT). get_entry({_Name, Type}=Field, Fields, Clock) -> - {Dots, CRDT, Tombstone} = case ?DICT:find(Field, Fields) of + {{Dots, _S, Tombstone}, CRDT} = case ?DICT:find(Field, Fields) of {ok, Entry} -> Entry; error -> - {[], Type:new(), riak_dt_vclock:fresh()} + {{[], [], riak_dt_vclock:fresh()}, Type:new()} end, - {Dots, Type:parent_clock(Clock, CRDT), Tombstone}. + {{Dots, _S, Tombstone}, Type:parent_clock(Clock, CRDT)}. %% @private -spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), @@ -369,11 +375,11 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), NewValues = case ?DICT:find(Field,Values) of - {ok, {_,_,Tombstone}} -> - ?DICT:store(Field, {[Dot], Updated, Tombstone}, Values); - error -> - ?DICT:store(Field, {[Dot], Updated, riak_dt_vclock:fresh()}, Values) - end, + {ok, {{_,_,Tombstone}, _}} -> + ?DICT:store(Field, {{[Dot], [], Tombstone}, Updated}, Values); + error -> + ?DICT:store(Field, {{[Dot], [], riak_dt_vclock:fresh()}, Updated}, Values) + end, apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); Error -> Error @@ -414,9 +420,9 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> NewValues = case ?DICT:find(Field, UpdtValues) of %Element is removed but has deferred operations {ok, empty} when DefCtx =/= no_deferred -> - ?DICT:update(Field, fun({Dots, CRDT, Tombstone}) -> - Tombstone = riak_dt_vclock:merge([DefCtx,Tombstone]), - {Dots, CRDT, Tombstone} + ?DICT:update(Field, fun({{Dots, _S, Tombstone}, CRDT}) -> + Tombstone = riak_dt_vclock:merge([DefCtx, Tombstone]), + {{Dots, _S, Tombstone}, CRDT} end,UpdtValues); {ok, empty} -> ?DICT:erase(Field, UpdtValues); @@ -431,7 +437,7 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> ctx_rem_field(_Field, error, _Ctx_, _Clock) -> empty; -ctx_rem_field({_, Type}, {ok, {Dots, CRDT, Tombstone}}, Ctx, MapClock) -> +ctx_rem_field({_, Type}, {ok, {{Dots, _S, Tombstone}, CRDT}}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% %% If the context is removing a field at dot {a, 1} and the @@ -450,7 +456,7 @@ ctx_rem_field({_, Type}, {ok, {Dots, CRDT, Tombstone}}, Ctx, MapClock) -> %% Update the tombstone with the GLB clock CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), %% Always reset to empty clock so we don't duplicate storage - {SurvivingDots, Type:parent_clock(?FRESH_CLOCK, CRDT2), Tombstone} + {{SurvivingDots, _S, Tombstone}, Type:parent_clock(?FRESH_CLOCK, CRDT2)} end; ctx_rem_field(Field, Values, Ctx, MapClock) -> ctx_rem_field(Field, ?DICT:find(Field, Values), Ctx, MapClock). @@ -458,10 +464,10 @@ ctx_rem_field(Field, Values, Ctx, MapClock) -> %% Value is a map: %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. -propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, MapClock, Ctx)-> +propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred}}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> - case propagate_remove(K, V, MapClock, Ctx) of + case propagate_remove(K, V, MapClock, Ctx) of {_, empty} -> {UpdtClock, UpdtEntries}; {TombstoneClock, Value} -> @@ -475,8 +481,7 @@ propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, case ?DICT:size(SubEntries) of 0 -> {SubMergedDef, empty}; - _ -> {SubMergedDef, {Dots, {Clock, SubEntries, Deferred}, - riak_dt_vclock:merge([SubMergedDef | Tombstone])}} + _ -> {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef | Tombstone])}, {Clock, SubEntries, Deferred}}} end; @@ -484,10 +489,10 @@ propagate_remove({_, riak_dt_map}, {Dots, {Clock, Value0, Deferred}, Tombstone}, %% Merge deferred operations' context with Value clock (Tombstone) and send it upstream %% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? %% Only handles half of the problem. -propagate_remove({_, Type}=Field, {Clock, CRDT, TombstoneIn}, MapClock, Ctx) -> +propagate_remove({_, Type}=Field, {{Clock, _S, TombstoneIn}, CRDT}, MapClock, Ctx) -> case Type:get_deferred(CRDT) of [] -> - {[], ctx_rem_field(Field, {ok, {Clock, CRDT, []}}, Ctx, MapClock)}; + {[], ctx_rem_field(Field, {ok, {{Clock, _S, []}, CRDT}}, Ctx, MapClock)}; _ -> Intersection = riak_dt_vclock:glb(Clock,Ctx), Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Type:get_deferred(CRDT)]), @@ -497,7 +502,7 @@ propagate_remove({_, Type}=Field, {Clock, CRDT, TombstoneIn}, MapClock, Ctx) -> TS = Type:parent_clock(TombstoneClock, Type:new()), ClearedCRDT = Type:merge(TS, Type:parent_clock(TombstoneClock, CRDT)), - {Tombstone, {Clock, ClearedCRDT, Tombstone}} + {Tombstone, {{Clock, _S, Tombstone}, ClearedCRDT}} end; propagate_remove(Field, Values, MapClock, Ctx) -> @@ -543,16 +548,17 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), Fields = lists:umerge(?DICT:fetch_keys(LHSEntries), ?DICT:fetch_keys(RHSEntries)), Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> - {LHSDots, LHSCRDT, LHDTomb} = get_entry(Field, LHSEntries, LHSClock), - {RHSDots, RHSCRDT, RHDTomb} = get_entry(Field, RHSEntries, RHSClock), + {{LHSDots, LHSS, LHDTomb}, LHSCRDT} = get_entry(Field, LHSEntries, LHSClock), + {{RHSDots, RHSS, RHDTomb}, RHSCRDT} = get_entry(Field, RHSEntries, RHSClock), case keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) of [] -> Acc; Dots -> - CRDT = Type:merge(LHSCRDT, RHSCRDT), + MergedCRDT = Type:merge(LHSCRDT, RHSCRDT), MergedTombstone = riak_dt_vclock:merge([LHDTomb,RHDTomb]), + MergedSeen = lists:umerge(LHSS, RHSS), %% Yes! Reset the clock, again - ?DICT:store(Field, {Dots, Type:parent_clock(?FRESH_CLOCK, CRDT), MergedTombstone}, Acc) + ?DICT:store(Field, {{Dots, MergedSeen, MergedTombstone}, Type:parent_clock(?FRESH_CLOCK, MergedCRDT)}, Acc) end end, ?DICT:new(), @@ -607,7 +613,7 @@ clear_tombstones({Clock, Entries, Deferred}) -> end, ?DICT:new(), Entries), {Clock, FilteredEntries, Deferred}. -clear_tombstones_handle({_, riak_dt_map}=Field, {Dots, {Clock, CRDT, Deferred}, Tombstone}, NewMap, MapClock) -> +clear_tombstones_handle({_, riak_dt_map}=Field, {{Dots, _S, Tombstone}, {Clock, CRDT, Deferred}}, NewMap, MapClock) -> FilteredEntries = ?DICT:fold(fun(Field_i, Value_i, FilteredEntriesAcc) -> clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, MapClock) @@ -615,17 +621,17 @@ clear_tombstones_handle({_, riak_dt_map}=Field, {Dots, {Clock, CRDT, Deferred}, %%Distinguish between empty map and removed map. case ?DICT:size(FilteredEntries) == 0 of true when length(Tombstone) > 0 -> NewMap; - _ -> ?DICT:store(Field, {Dots, {Clock, FilteredEntries, Deferred}, Tombstone}, NewMap) + _ -> ?DICT:store(Field, {{Dots, _S, Tombstone}, {Clock, FilteredEntries, Deferred}}, NewMap) end; -clear_tombstones_handle(Field, {Dots, CRDT, Tombstone}=Value, NewMap, MapClock) -> +clear_tombstones_handle(Field, {{Dots, _S, Tombstone}, CRDT}=Value, NewMap, MapClock) -> TombstoneCovered = riak_dt_vclock:descends(MapClock, Tombstone), case TombstoneCovered of true -> ReceivedUpdates = riak_dt_vclock:dominates(Dots, Tombstone), case ReceivedUpdates of true -> - ?DICT:store(Field, {Dots, CRDT, []}, NewMap); + ?DICT:store(Field, {{Dots, _S, []}, CRDT}, NewMap); false -> NewMap end; @@ -646,9 +652,9 @@ equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> -spec pairwise_equals(entries(), entries()) -> boolean(). pairwise_equals([], []) -> true; -pairwise_equals([{{Name, Type}, {Dots1, CRDT1,DeferredT1}}|Rest1], [{{Name, Type}, {Dots2, CRDT2, DeferredT2}}|Rest2]) -> - case {riak_dt_vclock:equal(Dots1, Dots2), Type:equal(CRDT1, CRDT2), riak_dt_vclock:equal(DeferredT1, DeferredT2)} of - {true, true, true} -> +pairwise_equals([{{Name, Type}, {{Dots1, S1, Tombstone1}, CRDT1}}|Rest1], [{{Name, Type}, {{Dots2, S2, Tombstone2}, CRDT2}}|Rest2]) -> + case {riak_dt_vclock:equal(Dots1, Dots2), S1 =:= S2, Type:equal(CRDT1, CRDT2), riak_dt_vclock:equal(Tombstone1, Tombstone2)} of + {true, true, true, true} -> pairwise_equals(Rest1, Rest2); _ -> false @@ -795,7 +801,7 @@ from_binary(?V2_VERS, <>) -> %% Issue 99 test case keep_deferred_test() -> - InitialState = riak_dt_map:new(), + InitialState = new(), %Update at node A {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), @@ -808,7 +814,7 @@ keep_deferred_test() -> %% Test that the field is preserved if a deferred arrives after a remove keep_multiple_deferred_test() -> - InitialState = riak_dt_map:new(), + InitialState = new(), %Update at node A {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), @@ -822,7 +828,7 @@ keep_multiple_deferred_test() -> %% Concurrently enable the flag keep_deferred_with_concurrent_add_test() -> - InitialState = riak_dt_map:new(), + InitialState = new(), %Update at node A {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), @@ -837,7 +843,7 @@ keep_deferred_with_concurrent_add_test() -> %% Remove using a context that does not descend from the object. %% Remove concurrent with deferred delete --- preserve remove keep_deferred_context_test() -> - InitialState = riak_dt_map:new(), + InitialState = new(), {ok, {CtxA1, _, _}=StateA1} = update({update, [{update, ?FIELD_X, enable}]}, a, InitialState), {ok, {_, _, _}=StateB1} = update({update, [{update, ?FIELD_X, disable}]}, b, InitialState, CtxA1), {ok, StateB2} = update({update, [{remove, ?FIELD_X}]}, b, StateB1, CtxA1), @@ -897,7 +903,7 @@ two_deferred_entries_test() -> {_,Map,_}=StateAB = merge(StateA1,StateB4), %%Check that the element is there - {ok, {_,{_,X,_},_}} = ?DICT:find(?FIELD,Map), + {ok, {{_,_,_},{_,X,_}}} = ?DICT:find(?FIELD,Map), ?assertEqual(true,?DICT:is_key(?FIELD_B,X)), StateABC = merge(StateAB,StateC1), ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). @@ -915,41 +921,41 @@ two_deferred_entries_2_test() -> ?assertEqual([{{'X',riak_dt_map},[]}],value(StateABC)). clear_invisible_after_merge_test() -> - InitialState = riak_dt_map:new(), - {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), - {ok, {_,_,_}=StateA2} = riak_dt_map:update(?ENABLE_FLAG_A, a, StateA1), - {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_A, b, InitialState, CtxA1), - {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_B, b, StateB1, CtxB1), - {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), - StateAB1 = riak_dt_map:merge(StateB3,StateA1), - StateAB2 = riak_dt_map:merge(StateB3,StateA2), - ?assertEqual([], riak_dt_map:value(StateAB1)), + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {_,_,_}=StateA2} = update(?ENABLE_FLAG_A, a, StateA1), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = update(?ENABLE_FLAG_B, b, StateB1, CtxB1), + {ok, {_,_,_}=StateB3} = update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + StateAB1 = merge(StateB3,StateA1), + StateAB2 = merge(StateB3,StateA2), + ?assertEqual([], value(StateAB1)), ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}]}], value(StateAB2)). clear_invisible_after_merge_2_test() -> - InitialState = riak_dt_map:new(), - {ok, {CtxA1,_,_}=_StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), - {ok, {_CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_B, b, InitialState,CtxA1), - {ok, {CtxB2,Map,_}=StateB2} = riak_dt_map:update(?ENABLE_FLAG_XYA, b, StateB1), + InitialState = new(), + {ok, {CtxA1,_,_}=_StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {_CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_B, b, InitialState,CtxA1), + {ok, {CtxB2,Map,_}=StateB2} = update(?ENABLE_FLAG_XYA, b, StateB1), %%Check that the element is there - {ok, {_,{_,X,_},_}} = ?DICT:find(?FIELD,Map), - {ok, {_,{_,Y,_},_}} = ?DICT:find(?FIELD_Y, X), + {ok, {{_,_,_},{_,X,_}}} = ?DICT:find(?FIELD,Map), + {ok, {{_,_,_},{_,Y,_}}} = ?DICT:find(?FIELD_Y, X), ?assertEqual(true,?DICT:is_key(?FIELD_A,Y)), - {ok, {_,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + {ok, {_,_,_}=StateB3} = update(?REMOVE_FIELD_X, b, StateB2, CtxB2), ?assertEqual([], value(StateB3)). clear_invisible_after_merge_set_test() -> AddElemToS = fun(Elem) -> {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {add, Elem}}]}}]} end, - InitialState = riak_dt_map:new(), - {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(?ENABLE_FLAG_A, a, InitialState), - {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(?DISABLE_FLAG_A, b, InitialState, CtxA1), - {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(AddElemToS(0), b, StateB1, CtxB1), - {ok, {CtxB3,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), - {ok, {_,_,_}=StateB4} = riak_dt_map:update(AddElemToS(1), b, StateB3, CtxB3), - StateAB = riak_dt_map:merge(StateA1,StateB4), + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = update(AddElemToS(0), b, StateB1, CtxB1), + {ok, {CtxB3,_,_}=StateB3} = update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + {ok, {_,_,_}=StateB4} = update(AddElemToS(1), b, StateB3, CtxB3), + StateAB = merge(StateA1,StateB4), ?assertEqual([{{'X',riak_dt_map}, [{{'X.S',riak_dt_orswot},[1]}]}], value(StateAB)). clear_invisible_after_merge_set_2_test() -> @@ -959,34 +965,34 @@ clear_invisible_after_merge_set_2_test() -> RemElemFromS = fun(Elem) -> {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {remove, Elem}}]}}]} end, - InitialState = riak_dt_map:new(), - {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(AddElemToS(0), a, InitialState), - {ok, {CtxB1,_,_}=StateB1} = riak_dt_map:update(RemElemFromS(0), b, InitialState, CtxA1), - {ok, {CtxB2,_,_}=StateB2} = riak_dt_map:update(AddElemToS(1), b, StateB1, CtxB1), - {ok, {CtxB3,_,_}=StateB3} = riak_dt_map:update(?REMOVE_FIELD_X, b, StateB2, CtxB2), - {ok, {_,_,_}=StateB4} = riak_dt_map:update(AddElemToS(2), b, StateB3, CtxB3), - StateAB = riak_dt_map:merge(StateA1,StateB4), + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(AddElemToS(0), a, InitialState), + {ok, {CtxB1,_,_}=StateB1} = update(RemElemFromS(0), b, InitialState, CtxA1), + {ok, {CtxB2,_,_}=StateB2} = update(AddElemToS(1), b, StateB1, CtxB1), + {ok, {CtxB3,_,_}=StateB3} = update(?REMOVE_FIELD_X, b, StateB2, CtxB2), + {ok, {_,_,_}=StateB4} = update(AddElemToS(2), b, StateB3, CtxB3), + StateAB = merge(StateA1,StateB4), ?assertEqual([{{'X',riak_dt_map}, [{{'X.S',riak_dt_orswot},[2]}]}], value(StateAB)). transaction_1_test() -> Updt1 = {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}, {update, ?FIELD_B, enable}]}}]}, - InitialState = riak_dt_map:new(), - {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(Updt1, a, InitialState), + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(Updt1, a, InitialState), ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, {{'X.B',riak_dt_od_flag},true}]}], value(StateA1)), - {ok, StateB1} = riak_dt_map:update(?REMOVE_FIELD_X, b, InitialState, CtxA1), - StateAB = riak_dt_map:merge(StateA1,StateB1), - ?assertEqual([], riak_dt_map:value(StateAB)). + {ok, StateB1} = update(?REMOVE_FIELD_X, b, InitialState, CtxA1), + StateAB = merge(StateA1,StateB1), + ?assertEqual([], value(StateAB)). transaction_2_test() -> Updt1 = {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}, {update, ?FIELD_B, enable}]}}]}, - InitialState = riak_dt_map:new(), - {ok, {CtxA1,_,_}=StateA1} = riak_dt_map:update(Updt1, a, InitialState), - {ok, StateB1} = riak_dt_map:update(?REMOVE_FIELD_XA, b, InitialState, CtxA1), - StateAB = riak_dt_map:merge(StateA1,StateB1), + InitialState = new(), + {ok, {CtxA1,_,_}=StateA1} = update(Updt1, a, InitialState), + {ok, StateB1} = update(?REMOVE_FIELD_XA, b, InitialState, CtxA1), + StateAB = merge(StateA1,StateB1), ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},true}]}], value(StateAB)), - {ok, StateA2} = riak_dt_map:update(?ENABLE_FLAG_A, a, StateA1), - StateA2B = riak_dt_map:merge(StateA2,StateB1), + {ok, StateA2} = update(?ENABLE_FLAG_A, a, StateA1), + StateA2B = merge(StateA2,StateB1), ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, {{'X.B',riak_dt_od_flag},true}]}], value(StateA2B)). From d2feb2e10f366cf222739b87ca4cadafd63d2fa8 Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 27 Jan 2015 23:09:29 +0000 Subject: [PATCH 21/33] Corrections taken from the delta implementation. --- src/riak_dt_map.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 602113a..6116443 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -619,7 +619,7 @@ clear_tombstones_handle({_, riak_dt_map}=Field, {{Dots, _S, Tombstone}, {Clock, clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, MapClock) end, ?DICT:new(), CRDT), %%Distinguish between empty map and removed map. - case ?DICT:size(FilteredEntries) == 0 of + case ?DICT:size(FilteredEntries) == 0 andalso riak_dt_vclock:descends(MapClock, Tombstone) of true when length(Tombstone) > 0 -> NewMap; _ -> ?DICT:store(Field, {{Dots, _S, Tombstone}, {Clock, FilteredEntries, Deferred}}, NewMap) end; @@ -628,12 +628,12 @@ clear_tombstones_handle(Field, {{Dots, _S, Tombstone}, CRDT}=Value, NewMap, MapC TombstoneCovered = riak_dt_vclock:descends(MapClock, Tombstone), case TombstoneCovered of true -> - ReceivedUpdates = riak_dt_vclock:dominates(Dots, Tombstone), + ReceivedUpdates = riak_dt_vclock:subtract_dots(Dots, Tombstone), case ReceivedUpdates of - true -> - ?DICT:store(Field, {{Dots, _S, []}, CRDT}, NewMap); - false -> - NewMap + [] -> + NewMap; + _ -> + ?DICT:store(Field, {{Dots, _S, []}, CRDT}, NewMap) end; false -> ?DICT:store(Field, Value, NewMap) From 34ae622d640a6c9373facffbde62d67565eaa718 Mon Sep 17 00:00:00 2001 From: balegas Date: Sat, 31 Jan 2015 02:18:17 +0000 Subject: [PATCH 22/33] WIP - Cleaning the code --- src/riak_dt_map.erl | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 6116443..75a9063 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -262,10 +262,11 @@ get_deferred({Clock, Entries, _}=CRDT, Ctx) -> %% @doc get the current set of values for this Map -spec value(map()) -> values(). -value({_Clock, Values, _Deferred}) -> +value({Clock, Values, _Deferred}) -> %%Selects the visible elements from the map lists:sort(?DICT:fold( - fun({Name, Type}, {{Dots, _, Tombstone}, CRDT}, Acc) -> + fun({Name, Type}, {{Dots, _, Tombstone}, CRDT0}, Acc) -> + CRDT = parent_clock(Clock, CRDT0), case Tombstone of [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; _ -> @@ -345,15 +346,6 @@ update_clock(Actor, Clock) -> Dot = {Actor, riak_dt_vclock:get_counter(Actor, NewClock)}, {Dot, NewClock}. -get({_Name, Type}=Field, Fields, Clock) -> - CRDT = case ?DICT:find(Field, Fields) of - {ok, {{_Dots, _, _},CRDT0}} -> - CRDT0; - error -> - Type:new() - end, - Type:parent_clock(Clock, CRDT). - get_entry({_Name, Type}=Field, Fields, Clock) -> {{Dots, _S, Tombstone}, CRDT} = case ?DICT:find(Field, Fields) of {ok, Entry} -> @@ -370,7 +362,7 @@ get_entry({_Name, Type}=Field, Fields, Clock) -> apply_ops([], _Dot, Map, _Ctx) -> {ok, Map}; apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> - CRDT = get(Field, Values, Clock), + {_, CRDT} = get_entry(Field, Values, Clock), case Type:update(Op, Dot, CRDT, Ctx) of {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), @@ -618,10 +610,22 @@ clear_tombstones_handle({_, riak_dt_map}=Field, {{Dots, _S, Tombstone}, {Clock, ?DICT:fold(fun(Field_i, Value_i, FilteredEntriesAcc) -> clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, MapClock) end, ?DICT:new(), CRDT), - %%Distinguish between empty map and removed map. - case ?DICT:size(FilteredEntries) == 0 andalso riak_dt_vclock:descends(MapClock, Tombstone) of - true when length(Tombstone) > 0 -> NewMap; - _ -> ?DICT:store(Field, {{Dots, _S, Tombstone}, {Clock, FilteredEntries, Deferred}}, NewMap) + %%Distinguish between empty map and removed map. --- this was changed, maybe do the same to map. + TombstoneCovered = riak_dt_vclock:descends(MapClock, Tombstone), + case ?DICT:size(FilteredEntries) == 0 of + true when length(Tombstone) > 0 andalso TombstoneCovered -> + % No childs, a tombstone was set (remove executed) and the clock dominates tombstone + NewMap; + _ -> + case TombstoneCovered of + true -> + % New entries were added to the map - keep entries, clear tomb + ?DICT:store(Field, {{Dots, _S, []}, {Clock, FilteredEntries, Deferred}}, NewMap); + false -> + % No childs, a tombstone was set, but the tombstone is still newer + % No childs, but no tombstonte was set (empty field) - keep all + ?DICT:store(Field, {{Dots, _S, Tombstone}, {Clock, FilteredEntries, Deferred}}, NewMap) + end end; clear_tombstones_handle(Field, {{Dots, _S, Tombstone}, CRDT}=Value, NewMap, MapClock) -> From c40b8fc6f478de845a2b4c0b333f094c09ab555a Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 3 Feb 2015 23:41:04 +0000 Subject: [PATCH 23/33] Ran Dialyzer Removed unnecessary code replaced raik_dt_vclock:fresh() by macro Removed whitespaces --- src/riak_dt.erl | 14 +--- src/riak_dt_map.erl | 161 ++++++++++++++++++++++---------------------- 2 files changed, 83 insertions(+), 92 deletions(-) diff --git a/src/riak_dt.erl b/src/riak_dt.erl index a95055a..8302f82 100644 --- a/src/riak_dt.erl +++ b/src/riak_dt.erl @@ -22,7 +22,7 @@ -module(riak_dt). --export([get_deferred/1, get_deferred/2, to_binary/1, from_binary/1, dict_to_orddict/1]). +-export([get_deferred/1, to_binary/1, from_binary/1, dict_to_orddict/1]). -export_type([actor/0, dot/0, crdt/0, context/0]). -type crdt() :: term(). @@ -44,8 +44,7 @@ %% given clock as it's own. -callback parent_clock(riak_dt_vclock:vclock(), crdt()) -> crdt(). --callback get_deferred(crdt()) -> [riak_dt_vclock:vclock()]. --callback get_deferred(crdt(), riak_dt_vclock:vclock()) -> [riak_dt_vclock:vclock()]. +-callback get_deferred(crdt()) -> [context()]. -callback merge(crdt(), crdt()) -> crdt(). -callback equal(crdt(), crdt()) -> boolean(). -callback to_binary(crdt()) -> binary(). @@ -87,12 +86,3 @@ dict_to_orddict(Dict) -> -spec get_deferred(crdt()) -> [riak_dt_vclock:vclock()]. get_deferred({_, _, Deferred}) -> Deferred. --spec get_deferred(crdt(), riak_dt_vclock:vclock()) -> [riak_dt_vclock:vclock()]. -get_deferred({_, _, Deferred}, Ctx) -> - lists:filtermap(fun(Def) -> - case riak_dt_vclock:glb(Def,Ctx) of - [] -> false; - Intersect -> {true, Intersect} - end - end, Deferred). - diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 75a9063..c264bd5 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -180,7 +180,7 @@ -export([merge/2, equal/2, to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1, stats/1, stat/2]). --export([parent_clock/2, get_deferred/1, get_deferred/2]). +-export([parent_clock/2, get_deferred/1]). %% EQC API -ifdef(EQC). @@ -191,23 +191,16 @@ -type binary_map() :: binary(). %% A binary that from_binary/1 will accept -type map() :: {riak_dt_vclock:vclock(), entries(), deferred()}. --type entries() :: [field()]. --type field() :: {field_name(), {field_meta(), field_value()}}. +-type entries() :: dict(field_name(), {field_meta(), field_value()}). -type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. -type field_meta() :: {riak_dt_vclock:vclock(), seen(), tombstone()}. -type field_value() :: crdt(). -%%-type crdts() :: [entry()]. -%%-type entry() :: {riak_dt:dot(), crdt()}. - -%% Only for present fields, ensures removes propogate -%%-type tombstone() :: crdt(). - %% Only field removals can be deferred. CRDTs stored in the map may %% have contexts and deferred operations, but as these are part of the %% state, they are stored under the field as an update like any other. -type seen() :: dots(). --type deferred() :: [{context(), [field()]}]. +-type deferred() :: dict(context(), [field_name()]). -type tombstone() :: riak_dt_vclock:vclock(). -type dots() :: [dot()]. @@ -216,18 +209,16 @@ %% limited to only those mods that support both a shared causal %% context, and by extension, the reset-remove semantic. -type crdt_mod() :: riak_dt_emcntr | riak_dt_lwwreg | -riak_dt_od_flag | -riak_dt_map | riak_dt_orswot. +riak_dt_od_flag | riak_dt_map | riak_dt_orswot. -type crdt() :: riak_dt_emcntr:emcntr() | riak_dt_od_flag:od_flag() | -riak_dt_lwwreg:lwwreg() | -riak_dt_orswot:orswot() | -riak_dt_map:map() | riak_dt_delta_map:map(). +riak_dt_lwwreg:lwwreg() | riak_dt_orswot:orswot() | riak_dt_map:map() | +riak_dt_delta_map:map(). -type map_op() :: {update, [map_field_update() | map_field_op()]}. --type map_field_op() :: {remove, field()}. --type map_field_update() :: {update, field(), crdt_op()}. +-type map_field_op() :: {remove, field_name()}. +-type map_field_update() :: {update, field_name(), crdt_op()}. -type crdt_op() :: riak_dt_emcntr:emcntr_op() | riak_dt_lwwreg:lwwreg_op() | @@ -237,8 +228,11 @@ riak_dt_map:map_op() | riak_dt_map:map_op(). -type context() :: riak_dt_vclock:vclock() | undefined. -type values() :: [value()]. --type value() :: {field(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. --type precondition_error() :: {error, {precondition, {not_present, field()}}}. +-type value() :: {field_name(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. +-type precondition_error() :: {error, {precondition, {not_present, field_name()}}}. + +%% used until we move to erlang 17 and can use dict:dict/2 +-type dict(_A, _B) :: dict(). -define(FRESH_CLOCK, riak_dt_vclock:fresh()). -define(DICT, dict). @@ -246,7 +240,7 @@ riak_dt_map:map_op() | riak_dt_map:map_op(). %% @doc Create a new, empty Map. -spec new() -> map(). new() -> - {riak_dt_vclock:fresh(), ?DICT:new(), ?DICT:new()}. + {?FRESH_CLOCK, ?DICT:new(), ?DICT:new()}. %% @doc sets the clock in the map to that `Clock'. Used by a %% containing Map for sub-CRDTs @@ -254,35 +248,37 @@ new() -> parent_clock(Clock, {_MapClock, Values, Deferred}) -> {Clock, Values, Deferred}. + +%% @doc get all deferred operations for the map. +%% Does not evaluate recursively - I think this is not necessary right now, +%% because we do not compose map with other data-types than maps. +-spec get_deferred(map()) -> [context()]. get_deferred({_, _, Deferred}) -> lists:map(fun({Key, _}) -> Key end, ?DICT:to_list(Deferred)). -get_deferred({Clock, Entries, _}=CRDT, Ctx) -> - riak_dt:get_deferred({Clock, Entries, get_deferred(CRDT)}, Ctx). - %% @doc get the current set of values for this Map -spec value(map()) -> values(). value({Clock, Values, _Deferred}) -> %%Selects the visible elements from the map lists:sort(?DICT:fold( - fun({Name, Type}, {{Dots, _, Tombstone}, CRDT0}, Acc) -> - CRDT = parent_clock(Clock, CRDT0), - case Tombstone of - [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; - _ -> - case riak_dt_vclock:descends(Tombstone, Dots) of - %No new dots in this branch - true -> - Acc; - %Dots in the branch - false -> - case Type of - %Recursive search - riak_dt_map -> - SubTree = value(CRDT), - case SubTree of - [] -> - Acc; + fun({Name, Type}, {{Dots, _, Tombstone}, CRDT0}, Acc) -> + CRDT = parent_clock(Clock, CRDT0), + case Tombstone of + [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; + _ -> + case riak_dt_vclock:descends(Tombstone, Dots) of + %No new dots in this branch + true -> + Acc; + %Dots in the branch + false -> + case Type of + %Recursive search + riak_dt_map -> + SubTree = value(CRDT), + case SubTree of + [] -> + Acc; _ -> [{{Name, Type}, Type:value(CRDT)} | Acc] end; _ -> @@ -346,12 +342,14 @@ update_clock(Actor, Clock) -> Dot = {Actor, riak_dt_vclock:get_counter(Actor, NewClock)}, {Dot, NewClock}. +-spec get_entry(field_name(), entries(), riak_dt_vclock:vclock()) -> + {field_meta(), field_value()}. get_entry({_Name, Type}=Field, Fields, Clock) -> {{Dots, _S, Tombstone}, CRDT} = case ?DICT:find(Field, Fields) of {ok, Entry} -> Entry; error -> - {{[], [], riak_dt_vclock:fresh()}, Type:new()} + {{[], [], ?FRESH_CLOCK}, Type:new()} end, {{Dots, _S, Tombstone}, Type:parent_clock(Clock, CRDT)}. @@ -370,7 +368,7 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer {ok, {{_,_,Tombstone}, _}} -> ?DICT:store(Field, {{[Dot], [], Tombstone}, Updated}, Values); error -> - ?DICT:store(Field, {{[Dot], [], riak_dt_vclock:fresh()}, Updated}, Values) + ?DICT:store(Field, {{[Dot], [], ?FRESH_CLOCK}, Updated}, Values) end, apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); Error -> @@ -396,7 +394,7 @@ apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> %% %% @see defer_remove/4 for handling of removes of fields that are %% _not_ present --spec remove_field(field(), map(), context()) -> +-spec remove_field(field_name(), map(), context()) -> {ok, map()} | precondition_error(). remove_field(Field, {Clock, Values, Deferred}, undefined) -> case ?DICT:find(Field, Values) of @@ -412,9 +410,13 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> NewValues = case ?DICT:find(Field, UpdtValues) of %Element is removed but has deferred operations {ok, empty} when DefCtx =/= no_deferred -> - ?DICT:update(Field, fun({{Dots, _S, Tombstone}, CRDT}) -> - Tombstone = riak_dt_vclock:merge([DefCtx, Tombstone]), - {{Dots, _S, Tombstone}, CRDT} + ?DICT:update(Field, fun(Found) -> + case Found of + {{Dots, _S, Tombstone}, CRDT} -> + Tombstone = riak_dt_vclock:merge([DefCtx, Tombstone]), + {{Dots, _S, Tombstone}, CRDT}; + error -> do_nothing + end end,UpdtValues); {ok, empty} -> ?DICT:erase(Field, UpdtValues); @@ -426,10 +428,8 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> {ok, {Clock, NewValues, Deferred}}. %% @private drop dominated fields -ctx_rem_field(_Field, error, _Ctx_, _Clock) -> - empty; - -ctx_rem_field({_, Type}, {ok, {{Dots, _S, Tombstone}, CRDT}}, Ctx, MapClock) -> +-spec ctx_rem_field(field_name(), {field_meta(), field_value()} , context(), riak_dt_vclock:vclock()) -> empty | {field_meta(), field_value()}. +ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% %% If the context is removing a field at dot {a, 1} and the @@ -449,26 +449,25 @@ ctx_rem_field({_, Type}, {ok, {{Dots, _S, Tombstone}, CRDT}}, Ctx, MapClock) -> CRDT2 = Type:merge(TS, Type:parent_clock(MapClock, CRDT)), %% Always reset to empty clock so we don't duplicate storage {{SurvivingDots, _S, Tombstone}, Type:parent_clock(?FRESH_CLOCK, CRDT2)} - end; -ctx_rem_field(Field, Values, Ctx, MapClock) -> - ctx_rem_field(Field, ?DICT:find(Field, Values), Ctx, MapClock). + end. %% Value is a map: %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. +-spec propagate_remove(field_name(), entries() | {field_meta(), field_value()}, riak_dt_vclock:vclock(), context()) -> {riak_dt_vclock:vclock(), entries() | empty}. propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred}}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> case propagate_remove(K, V, MapClock, Ctx) of {_, empty} -> {UpdtClock, UpdtEntries}; - {TombstoneClock, Value} -> + {TombstoneClock, Value} -> %%Some deferred operation in subtree %% - keep entry, update tombstones - {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), + {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), ?DICT:store(K, Value, UpdtEntries)} end - end, {riak_dt_vclock:fresh(),?DICT:new()},Value0), + end, {?FRESH_CLOCK, ?DICT:new()}, Value0), %Clear map if all entries are empty case ?DICT:size(SubEntries) of 0 -> @@ -481,25 +480,25 @@ propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Defer %% Merge deferred operations' context with Value clock (Tombstone) and send it upstream %% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? %% Only handles half of the problem. -propagate_remove({_, Type}=Field, {{Clock, _S, TombstoneIn}, CRDT}, MapClock, Ctx) -> +propagate_remove({_, Type}=Field, {{Dots, _S, TombstoneIn}, CRDT}, MapClock, Ctx) -> case Type:get_deferred(CRDT) of [] -> - {[], ctx_rem_field(Field, {ok, {{Clock, _S, []}, CRDT}}, Ctx, MapClock)}; - _ -> - Intersection = riak_dt_vclock:glb(Clock,Ctx), - Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Type:get_deferred(CRDT)]), - + {[], ctx_rem_field(Field, {{Dots, _S, []}, CRDT}, Ctx, MapClock)}; + Deferred -> + Intersection = riak_dt_vclock:glb(MapClock,Ctx), + Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Deferred]), + %% Clear CRDT TombstoneClock = riak_dt_vclock:glb(MapClock, Ctx), TS = Type:parent_clock(TombstoneClock, Type:new()), ClearedCRDT = Type:merge(TS, Type:parent_clock(TombstoneClock, CRDT)), - - {Tombstone, {{Clock, _S, Tombstone}, ClearedCRDT}} + + {Tombstone, {{Dots, _S, Tombstone}, ClearedCRDT}} end; propagate_remove(Field, Values, MapClock, Ctx) -> case ?DICT:find(Field, Values) of - {ok,Value} -> + {ok,Value} -> {UpdtClock,UpdtValue} = propagate_remove(Field, Value, MapClock, Ctx), case UpdtValue of empty -> @@ -523,7 +522,7 @@ propagate_remove(Field, Values, MapClock, Ctx) -> %% result in deferred operations on the parent Map. This simulates %% causal delivery, in that an `update' must be seen before it can be %% `removed'. --spec defer_remove(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), field(), deferred()) -> +-spec defer_remove(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), field_name(), deferred()) -> deferred(). defer_remove(Clock, Ctx, Field, Deferred) -> case riak_dt_vclock:descends(Clock, Ctx) of @@ -535,7 +534,7 @@ defer_remove(Clock, Ctx, Field, Deferred) -> ordsets:add_element(Field, ordsets:new()), Deferred) end. - +-spec merge(map(), map()) -> map(). merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) -> Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), Fields = lists:umerge(?DICT:fetch_keys(LHSEntries), ?DICT:fetch_keys(RHSEntries)), @@ -559,6 +558,7 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) CRDT = apply_deferred(Clock, Entries, Deferred), clear_tombstones(CRDT). +-spec keep_dots(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), riak_dt_vclock:vclock()) -> riak_dt_vclock:vclock(). keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> CommonDots = sets:intersection(sets:from_list(LHSDots), sets:from_list(RHSDots)), LHSUnique = sets:to_list(sets:subtract(sets:from_list(LHSDots), CommonDots)), @@ -587,7 +587,7 @@ apply_deferred(Clock, Entries, Deferred) -> Deferred). %% @private --spec remove_all([field()], map(), context()) -> +-spec remove_all([field_name()], map(), context()) -> map(). remove_all(Fields, Map, Ctx) -> lists:foldl(fun(Field, MapAcc) -> @@ -596,17 +596,18 @@ remove_all(Fields, Map, Ctx) -> end, Map, Fields). - -%Eliminates the tombstone if it has been integrated in the object's clock. +%% @private +%%Eliminates the tombstone if it has been integrated in the object's clock. +-spec clear_tombstones(map()) -> map(). clear_tombstones({Clock, Entries, Deferred}) -> - FilteredEntries = + FilteredEntries = ?DICT:fold(fun(Field_i, Value_i, FilteredEntriesAcc) -> clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, Clock) end, ?DICT:new(), Entries), {Clock, FilteredEntries, Deferred}. clear_tombstones_handle({_, riak_dt_map}=Field, {{Dots, _S, Tombstone}, {Clock, CRDT, Deferred}}, NewMap, MapClock) -> - FilteredEntries = + FilteredEntries = ?DICT:fold(fun(Field_i, Value_i, FilteredEntriesAcc) -> clear_tombstones_handle(Field_i, Value_i, FilteredEntriesAcc, MapClock) end, ?DICT:new(), CRDT), @@ -622,7 +623,7 @@ clear_tombstones_handle({_, riak_dt_map}=Field, {{Dots, _S, Tombstone}, {Clock, % New entries were added to the map - keep entries, clear tomb ?DICT:store(Field, {{Dots, _S, []}, {Clock, FilteredEntries, Deferred}}, NewMap); false -> - % No childs, a tombstone was set, but the tombstone is still newer + % A tombstone was set, but the tombstone is still newer % No childs, but no tombstonte was set (empty field) - keep all ?DICT:store(Field, {{Dots, _S, Tombstone}, {Clock, FilteredEntries, Deferred}}, NewMap) end @@ -634,12 +635,12 @@ clear_tombstones_handle(Field, {{Dots, _S, Tombstone}, CRDT}=Value, NewMap, MapC true -> ReceivedUpdates = riak_dt_vclock:subtract_dots(Dots, Tombstone), case ReceivedUpdates of - [] -> + [] -> NewMap; _ -> ?DICT:store(Field, {{Dots, _S, []}, CRDT}, NewMap) end; - false -> + false -> ?DICT:store(Field, Value, NewMap) end. @@ -874,7 +875,7 @@ remove_entry_in_subtree_test() -> StateAB = merge(StateA1,StateB2), ?assertEqual([{{'X',riak_dt_map},[]}],value(StateAB)). -%% Remove a field X that is a map that has a entry with a deferred +%% Remove a field X that is a map that has a entry with a deferred %% and a flag that is enable after remove X but before receiving the deferred. %% The idea is to test that the flag is preserved after clearing the tombstones remove_entry_in_subtree_2_test() -> @@ -955,7 +956,7 @@ clear_invisible_after_merge_set_test() -> end, InitialState = new(), {ok, {CtxA1,_,_}=StateA1} = update(?ENABLE_FLAG_A, a, InitialState), - {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), + {ok, {CtxB1,_,_}=StateB1} = update(?DISABLE_FLAG_A, b, InitialState, CtxA1), {ok, {CtxB2,_,_}=StateB2} = update(AddElemToS(0), b, StateB1, CtxB1), {ok, {CtxB3,_,_}=StateB3} = update(?REMOVE_FIELD_X, b, StateB2, CtxB2), {ok, {_,_,_}=StateB4} = update(AddElemToS(1), b, StateB3, CtxB3), @@ -982,7 +983,7 @@ transaction_1_test() -> Updt1 = {update, [{update, ?FIELD, {update, [{update, ?FIELD_A, enable}, {update, ?FIELD_B, enable}]}}]}, InitialState = new(), {ok, {CtxA1,_,_}=StateA1} = update(Updt1, a, InitialState), - ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, + ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, {{'X.B',riak_dt_od_flag},true}]}], value(StateA1)), {ok, StateB1} = update(?REMOVE_FIELD_X, b, InitialState, CtxA1), StateAB = merge(StateA1,StateB1), @@ -997,7 +998,7 @@ transaction_2_test() -> ?assertEqual([{{'X',riak_dt_map},[{{'X.B',riak_dt_od_flag},true}]}], value(StateAB)), {ok, StateA2} = update(?ENABLE_FLAG_A, a, StateA1), StateA2B = merge(StateA2,StateB1), - ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, + ?assertEqual([{{'X',riak_dt_map},[{{'X.A',riak_dt_od_flag},true}, {{'X.B',riak_dt_od_flag},true}]}], value(StateA2B)). %% This fails on previous version of riak_dt_map From 6b2f2388bf6f5dd9d3a5684881f8e79110e4f525 Mon Sep 17 00:00:00 2001 From: balegas Date: Wed, 4 Feb 2015 12:02:57 +0000 Subject: [PATCH 24/33] Removed generic riak_dt:get_deferred/1 implementation and removed get_deferred/2 from the interface. Fixed some issues with map dialyzer. --- src/riak_dt.erl | 11 +++-------- src/riak_dt_disable_flag.erl | 11 ++++------- src/riak_dt_emcntr.erl | 4 +--- src/riak_dt_enable_flag.erl | 5 ++--- src/riak_dt_gcounter.erl | 5 ++--- src/riak_dt_gset.erl | 5 ++--- src/riak_dt_lwwreg.erl | 5 ++--- src/riak_dt_map.erl | 31 +++++++++++-------------------- src/riak_dt_od_flag.erl | 9 +++------ src/riak_dt_oe_flag.erl | 5 ++--- src/riak_dt_orset.erl | 5 ++--- src/riak_dt_orswot.erl | 6 ++---- src/riak_dt_pncounter.erl | 5 ++--- 13 files changed, 38 insertions(+), 69 deletions(-) diff --git a/src/riak_dt.erl b/src/riak_dt.erl index 8302f82..07a3bd4 100644 --- a/src/riak_dt.erl +++ b/src/riak_dt.erl @@ -22,7 +22,7 @@ -module(riak_dt). --export([get_deferred/1, to_binary/1, from_binary/1, dict_to_orddict/1]). +-export([to_binary/1, from_binary/1, dict_to_orddict/1]). -export_type([actor/0, dot/0, crdt/0, context/0]). -type crdt() :: term(). @@ -43,13 +43,13 @@ %% the clock and the crdt, and if relevant, returns to crdt with the %% given clock as it's own. -callback parent_clock(riak_dt_vclock:vclock(), crdt()) -> - crdt(). + crdt(). -callback get_deferred(crdt()) -> [context()]. -callback merge(crdt(), crdt()) -> crdt(). -callback equal(crdt(), crdt()) -> boolean(). -callback to_binary(crdt()) -> binary(). -callback to_binary(TargetVers :: pos_integer(), crdt()) -> - binary(). + binary(). -callback from_binary(binary()) -> crdt(). -callback from_binary(TargetVers :: pos_integer(), binary()) -> crdt(). @@ -77,12 +77,7 @@ to_binary(Term) -> from_binary(Binary) -> binary_to_term(Binary). - %% @private -spec dict_to_orddict(dict()) -> orddict:orddict(). dict_to_orddict(Dict) -> orddict:from_list(dict:to_list(Dict)). - --spec get_deferred(crdt()) -> [riak_dt_vclock:vclock()]. -get_deferred({_, _, Deferred}) -> Deferred. - diff --git a/src/riak_dt_disable_flag.erl b/src/riak_dt_disable_flag.erl index d11def6..9a48fc6 100644 --- a/src/riak_dt_disable_flag.erl +++ b/src/riak_dt_disable_flag.erl @@ -30,7 +30,7 @@ -behaviour(riak_dt). -export([new/0, value/1, value/2, update/3, merge/2, equal/2, from_binary/1, to_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). -include_lib("eqc/include/eqc.hrl"). @@ -65,7 +65,7 @@ update(disable, _Actor, _Flag) -> {ok, off}. -spec update(disable_flag_op(), riak_dt:actor(), disable_flag(), riak_dt:context()) -> - {ok, disable_flag()}. + {ok, disable_flag()}. update(Op, Actor, Flag, _Ctx) -> update(Op, Actor, Flag). @@ -82,11 +82,8 @@ equal(FA,FB) -> parent_clock(_Clock, Flag) -> Flag. -get_deferred(CRDT) -> - riak_dt:get_deferred(CRDT). - -get_deferred(CRDT, Ctx) -> - riak_dt:get_deferred(CRDT, Ctx). +-spec get_deferred(disable_flag()) -> []. +get_deferred(_Flag) -> []. -spec from_binary(binary()) -> disable_flag(). from_binary(<>) -> off; diff --git a/src/riak_dt_emcntr.erl b/src/riak_dt_emcntr.erl index 4a3119b..88ef61e 100644 --- a/src/riak_dt_emcntr.erl +++ b/src/riak_dt_emcntr.erl @@ -50,7 +50,7 @@ -export([to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). %% EQC API -ifdef(EQC). @@ -87,8 +87,6 @@ parent_clock(Clock, {_, Cntr}) -> get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - %% @doc the current integer value of the counter -spec value(emcntr()) -> integer(). value({_Clock, PNCnt}) -> diff --git a/src/riak_dt_enable_flag.erl b/src/riak_dt_enable_flag.erl index c0d59b9..a3d7966 100644 --- a/src/riak_dt_enable_flag.erl +++ b/src/riak_dt_enable_flag.erl @@ -30,7 +30,7 @@ -behaviour(riak_dt). -export([new/0, value/1, value/2, update/3, merge/2, equal/2, from_binary/1, to_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -74,10 +74,9 @@ update(enable, _Actor, _Flag, _Ctx) -> parent_clock(_Clock, Flag) -> Flag. +-spec get_deferred(enable_flag()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - -spec merge(enable_flag(), enable_flag()) -> enable_flag(). merge(FA, FB) -> flag_or(FA, FB). diff --git a/src/riak_dt_gcounter.erl b/src/riak_dt_gcounter.erl index 926d644..3eca767 100644 --- a/src/riak_dt_gcounter.erl +++ b/src/riak_dt_gcounter.erl @@ -37,7 +37,7 @@ -module(riak_dt_gcounter). -behaviour(riak_dt). -export([new/0, new/2, value/1, value/2, update/3, merge/2, equal/2, to_binary/1, from_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). %% EQC API @@ -95,10 +95,9 @@ update(Op, Actor, GCnt, _Ctx) -> parent_clock(_Clock, GCnt) -> GCnt. +-spec get_deferred(gcounter()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - %% @doc Merge two `gcounter()'s to a single `gcounter()'. This is the Least Upper Bound %% function described in the literature. -spec merge(gcounter(), gcounter()) -> gcounter(). diff --git a/src/riak_dt_gset.erl b/src/riak_dt_gset.erl index faf3520..f8c044c 100644 --- a/src/riak_dt_gset.erl +++ b/src/riak_dt_gset.erl @@ -35,7 +35,7 @@ %% API -export([new/0, value/1, update/3, merge/2, equal/2, to_binary/1, from_binary/1, value/2, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -92,10 +92,9 @@ update(Op, Actor, GSet, _Ctx) -> parent_clock(_Clock, GSet) -> GSet. +-spec get_deferred(gset()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - -spec merge(gset(), gset()) -> gset(). merge(GSet1, GSet2) -> ordsets:union(GSet1, GSet2). diff --git a/src/riak_dt_lwwreg.erl b/src/riak_dt_lwwreg.erl index d2123aa..53c4571 100644 --- a/src/riak_dt_lwwreg.erl +++ b/src/riak_dt_lwwreg.erl @@ -33,7 +33,7 @@ -export([new/0, value/1, value/2, update/3, merge/2, equal/2, to_binary/1, from_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). %% EQC API @@ -63,10 +63,9 @@ new() -> parent_clock(_Clock, Reg) -> Reg. +-spec get_deferred(lwwreg()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - %% @doc The single total value of a `gcounter()'. -spec value(lwwreg()) -> term(). value({Value, _TS}) -> diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index c264bd5..73d40b6 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -200,7 +200,7 @@ %% have contexts and deferred operations, but as these are part of the %% state, they are stored under the field as an update like any other. -type seen() :: dots(). --type deferred() :: dict(context(), [field_name()]). +-type deferred() :: dict(riak_dt:context(), [field_name()]). -type tombstone() :: riak_dt_vclock:vclock(). -type dots() :: [dot()]. @@ -212,8 +212,7 @@ riak_dt_od_flag | riak_dt_map | riak_dt_orswot. -type crdt() :: riak_dt_emcntr:emcntr() | riak_dt_od_flag:od_flag() | -riak_dt_lwwreg:lwwreg() | riak_dt_orswot:orswot() | riak_dt_map:map() | -riak_dt_delta_map:map(). +riak_dt_lwwreg:lwwreg() | riak_dt_orswot:orswot() | riak_dt_map:map(). -type map_op() :: {update, [map_field_update() | map_field_op()]}. @@ -225,8 +224,6 @@ riak_dt_lwwreg:lwwreg_op() | riak_dt_orswot:orswot_op() | riak_dt_od_flag:od_flag_op() | riak_dt_map:map_op() | riak_dt_map:map_op(). --type context() :: riak_dt_vclock:vclock() | undefined. - -type values() :: [value()]. -type value() :: {field_name(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. -type precondition_error() :: {error, {precondition, {not_present, field_name()}}}. @@ -248,11 +245,10 @@ new() -> parent_clock(Clock, {_MapClock, Values, Deferred}) -> {Clock, Values, Deferred}. - %% @doc get all deferred operations for the map. %% Does not evaluate recursively - I think this is not necessary right now, %% because we do not compose map with other data-types than maps. --spec get_deferred(map()) -> [context()]. +-spec get_deferred(map()) -> [riak_dt:context()]. get_deferred({_, _, Deferred}) -> lists:map(fun({Key, _}) -> Key end, ?DICT:to_list(Deferred)). @@ -355,7 +351,7 @@ get_entry({_Name, Type}=Field, Fields, Clock) -> %% @private -spec apply_ops([map_field_update() | map_field_op()], riak_dt:dot(), - {riak_dt_vclock:vclock(), entries() , deferred()}, context()) -> + {riak_dt_vclock:vclock(), entries() , deferred()}, riak_dt:context()) -> {ok, map()} | precondition_error(). apply_ops([], _Dot, Map, _Ctx) -> {ok, Map}; @@ -394,7 +390,7 @@ apply_ops([{remove, Field} | Rest], Dot, Map, Ctx) -> %% %% @see defer_remove/4 for handling of removes of fields that are %% _not_ present --spec remove_field(field_name(), map(), context()) -> +-spec remove_field(field_name(), map(), riak_dt:context()) -> {ok, map()} | precondition_error(). remove_field(Field, {Clock, Values, Deferred}, undefined) -> case ?DICT:find(Field, Values) of @@ -415,7 +411,7 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> {{Dots, _S, Tombstone}, CRDT} -> Tombstone = riak_dt_vclock:merge([DefCtx, Tombstone]), {{Dots, _S, Tombstone}, CRDT}; - error -> do_nothing + error -> UpdtValues end end,UpdtValues); {ok, empty} -> @@ -428,7 +424,7 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> {ok, {Clock, NewValues, Deferred}}. %% @private drop dominated fields --spec ctx_rem_field(field_name(), {field_meta(), field_value()} , context(), riak_dt_vclock:vclock()) -> empty | {field_meta(), field_value()}. +-spec ctx_rem_field(field_name(), {field_meta(), field_value()} , riak_dt:context(), riak_dt_vclock:vclock()) -> empty | {field_meta(), field_value()}. ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% @@ -454,7 +450,7 @@ ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Value is a map: %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. --spec propagate_remove(field_name(), entries() | {field_meta(), field_value()}, riak_dt_vclock:vclock(), context()) -> {riak_dt_vclock:vclock(), entries() | empty}. +-spec propagate_remove(field_name(), entries() | {field_meta(), field_value()}, riak_dt_vclock:vclock(), riak_dt:context()) -> {riak_dt_vclock:vclock(), entries() | empty}. propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred}}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> @@ -475,11 +471,8 @@ propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Defer _ -> {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef | Tombstone])}, {Clock, SubEntries, Deferred}}} end; - %% Value is a leaf: %% Merge deferred operations' context with Value clock (Tombstone) and send it upstream -%% Exclude non-covered dots -- intersection -- how does this relate to the second TODO? -%% Only handles half of the problem. propagate_remove({_, Type}=Field, {{Dots, _S, TombstoneIn}, CRDT}, MapClock, Ctx) -> case Type:get_deferred(CRDT) of [] -> @@ -587,7 +580,7 @@ apply_deferred(Clock, Entries, Deferred) -> Deferred). %% @private --spec remove_all([field_name()], map(), context()) -> +-spec remove_all([field_name()], map(), riak_dt:context()) -> map(). remove_all(Fields, Map, Ctx) -> lists:foldl(fun(Field, MapAcc) -> @@ -654,7 +647,7 @@ equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> pairwise_equals(lists:sort(?DICT:to_list(Values1)), lists:sort(?DICT:to_list(Values2))). --spec pairwise_equals(entries(), entries()) -> boolean(). +-spec pairwise_equals(entries() | [], entries() | []) -> boolean(). pairwise_equals([], []) -> true; pairwise_equals([{{Name, Type}, {{Dots1, S1, Tombstone1}, CRDT1}}|Rest1], [{{Name, Type}, {{Dots2, S2, Tombstone2}, CRDT2}}|Rest2]) -> @@ -663,9 +656,7 @@ pairwise_equals([{{Name, Type}, {{Dots1, S1, Tombstone1}, CRDT1}}|Rest1], [{{Nam pairwise_equals(Rest1, Rest2); _ -> false - end; -pairwise_equals(_, _) -> - false. + end. %% @doc an opaque context that can be passed to `update/4' to ensure %% that only seen fields are removed. If a field removal operation has diff --git a/src/riak_dt_od_flag.erl b/src/riak_dt_od_flag.erl index 67dcad2..ca4469a 100644 --- a/src/riak_dt_od_flag.erl +++ b/src/riak_dt_od_flag.erl @@ -30,7 +30,7 @@ -export([to_binary/1, stats/1, stat/2]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1]). --export([parent_clock/2, get_deferred/1, get_deferred/2, clear/1, clear/2]). +-export([parent_clock/2, get_deferred/1, clear/1, clear/2]). -ifdef(EQC). -include_lib("eqc/include/eqc.hrl"). @@ -58,11 +58,8 @@ new() -> parent_clock(Clock, {_SetClock, Flag , Deferred}) -> {Clock, Flag, Deferred}. -get_deferred(CRDT) -> - riak_dt:get_deferred(CRDT). - -get_deferred(CRDT, Ctx) -> - riak_dt:get_deferred(CRDT, Ctx). +-spec get_deferred(od_flag()) -> [riak_dt:context()]. +get_deferred({_, _, Deferred}) -> Deferred. -spec value(od_flag()) -> boolean(). value({_, [], _}) -> false; diff --git a/src/riak_dt_oe_flag.erl b/src/riak_dt_oe_flag.erl index 20c8562..0cc95e6 100644 --- a/src/riak_dt_oe_flag.erl +++ b/src/riak_dt_oe_flag.erl @@ -26,7 +26,7 @@ -behaviour(riak_dt). -export([new/0, value/1, value/2, update/3, merge/2, equal/2, from_binary/1, to_binary/1, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -72,10 +72,9 @@ update(Op, Actor, Flag, _Ctx) -> parent_clock(_Clock, Flag) -> Flag. +-spec get_deferred(oe_flag()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - -spec merge(oe_flag(), oe_flag()) -> oe_flag(). merge({C1, F}, {C2,F}) -> %% When they are the same result (true or false), just merge the diff --git a/src/riak_dt_orset.erl b/src/riak_dt_orset.erl index f49c84c..e637050 100644 --- a/src/riak_dt_orset.erl +++ b/src/riak_dt_orset.erl @@ -27,7 +27,7 @@ %% API -export([new/0, value/1, update/3, merge/2, equal/2, to_binary/1, from_binary/1, value/2, precondition_context/1, stats/1, stat/2]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). -export([to_binary/2, from_binary/2]). -ifdef(EQC). @@ -118,10 +118,9 @@ update(Op, Actor, ORDict, _Ctx) -> parent_clock(_Clock, ORSet) -> ORSet. +-spec get_deferred(orset()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - -spec merge(orset(), orset()) -> orset(). merge(ORDictA, ORDictB) -> orddict:merge(fun(_Elem,TokensA,TokensB) -> diff --git a/src/riak_dt_orswot.erl b/src/riak_dt_orswot.erl index 1968a4b..7eacb08 100644 --- a/src/riak_dt_orswot.erl +++ b/src/riak_dt_orswot.erl @@ -80,7 +80,7 @@ -export([to_binary/1, from_binary/1]). -export([to_binary/2, from_binary/2]). -export([precondition_context/1, stats/1, stat/2]). --export([parent_clock/2, get_deferred/1, get_deferred/2, clear/1, clear/2]). +-export([parent_clock/2, get_deferred/1, clear/1, clear/2]). %% EQC API -ifdef(EQC). @@ -132,12 +132,10 @@ new() -> parent_clock(Clock, {_SetClock, Entries, Deferred}) -> {Clock, Entries, Deferred}. +-spec get_deferred(orswot()) -> [riak_dt:context()]. get_deferred({_, _, Deferred}) -> lists:map(fun({Key, _}) -> Key end, ?DICT:to_list(Deferred)). -get_deferred({Clock, Entries, _}=CRDT, Ctx) -> - riak_dt:get_deferred({Clock, Entries, get_deferred(CRDT)}, Ctx). - -spec value(orswot()) -> [member()]. value({_Clock, Entries, _Deferred}) -> lists:sort([K || {K, _Dots} <- ?DICT:to_list(Entries)]). diff --git a/src/riak_dt_pncounter.erl b/src/riak_dt_pncounter.erl index fb72ef4..a2d8c82 100644 --- a/src/riak_dt_pncounter.erl +++ b/src/riak_dt_pncounter.erl @@ -38,7 +38,7 @@ -export([new/0, new/2, value/1, value/2, update/3, merge/2, equal/2, to_binary/1, from_binary/1, stats/1, stat/2]). -export([to_binary/2, from_binary/2, current_version/1, change_versions/3]). --export([update/4, parent_clock/2, get_deferred/1, get_deferred/2]). +-export([update/4, parent_clock/2, get_deferred/1]). %% EQC API -ifdef(EQC). @@ -81,10 +81,9 @@ new(_Actor, _Zero) -> parent_clock(_Clock, Cntr) -> Cntr. +-spec get_deferred(pncounter()) -> []. get_deferred(_CRDT) -> []. -get_deferred(_CRDT, _Ctx) -> []. - %% @doc The single, total value of a `pncounter()' -spec value(pncounter()) -> integer(). value(PNCnt) -> From 9d0919554e71f519268f9492cffd0c91019692f6 Mon Sep 17 00:00:00 2001 From: balegas Date: Fri, 6 Feb 2015 00:18:18 +0000 Subject: [PATCH 25/33] Changed some type names. --- src/riak_dt_map.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 73d40b6..2ab1595 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -191,10 +191,10 @@ -type binary_map() :: binary(). %% A binary that from_binary/1 will accept -type map() :: {riak_dt_vclock:vclock(), entries(), deferred()}. --type entries() :: dict(field_name(), {field_meta(), field_value()}). +-type entries() :: dict(field_name(), field_value()). -type field_name() :: {Name :: binary(), CRDTModule :: crdt_mod()}. -type field_meta() :: {riak_dt_vclock:vclock(), seen(), tombstone()}. --type field_value() :: crdt(). +-type field_value() :: {field_meta(), crdt()}. %% Only field removals can be deferred. CRDTs stored in the map may %% have contexts and deferred operations, but as these are part of the @@ -339,7 +339,7 @@ update_clock(Actor, Clock) -> {Dot, NewClock}. -spec get_entry(field_name(), entries(), riak_dt_vclock:vclock()) -> - {field_meta(), field_value()}. + field_value(). get_entry({_Name, Type}=Field, Fields, Clock) -> {{Dots, _S, Tombstone}, CRDT} = case ?DICT:find(Field, Fields) of {ok, Entry} -> @@ -424,7 +424,7 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> {ok, {Clock, NewValues, Deferred}}. %% @private drop dominated fields --spec ctx_rem_field(field_name(), {field_meta(), field_value()} , riak_dt:context(), riak_dt_vclock:vclock()) -> empty | {field_meta(), field_value()}. +-spec ctx_rem_field(field_name(), field_value(), riak_dt:context(), riak_dt_vclock:vclock()) -> empty | field_value(). ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Drop dominated fields, and update the tombstone. %% @@ -450,7 +450,7 @@ ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Value is a map: %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. --spec propagate_remove(field_name(), entries() | {field_meta(), field_value()}, riak_dt_vclock:vclock(), riak_dt:context()) -> {riak_dt_vclock:vclock(), entries() | empty}. +-spec propagate_remove(field_name(), entries() | field_value(), riak_dt_vclock:vclock(), riak_dt:context()) -> {riak_dt_vclock:vclock(), entries() | empty}. propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred}}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> From 6aea55f502773d8e683308ee1220248763d9e90a Mon Sep 17 00:00:00 2001 From: balegas Date: Tue, 10 Feb 2015 15:45:17 +0000 Subject: [PATCH 26/33] Fixed wrong merge - error found by QuickCheck --- src/riak_dt_map.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 2ab1595..e8e267f 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -468,7 +468,7 @@ propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Defer case ?DICT:size(SubEntries) of 0 -> {SubMergedDef, empty}; - _ -> {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef | Tombstone])}, {Clock, SubEntries, Deferred}}} + _ -> {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef, Tombstone])}, {Clock, SubEntries, Deferred}}} end; %% Value is a leaf: From 8756f680b41493905af26eec54296aad2995915a Mon Sep 17 00:00:00 2001 From: Russell Brown Date: Tue, 10 Feb 2015 15:50:58 +0000 Subject: [PATCH 27/33] Set entries parent clock. --- src/riak_dt_map.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index e8e267f..8d0287e 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -258,7 +258,7 @@ value({Clock, Values, _Deferred}) -> %%Selects the visible elements from the map lists:sort(?DICT:fold( fun({Name, Type}, {{Dots, _, Tombstone}, CRDT0}, Acc) -> - CRDT = parent_clock(Clock, CRDT0), + CRDT = Type:parent_clock(Clock, CRDT0), case Tombstone of [] -> [{{Name, Type}, Type:value(CRDT)} | Acc]; _ -> From 4f145678efff55e84b6a994579d6d668a638bfe2 Mon Sep 17 00:00:00 2001 From: balegas Date: Wed, 11 Feb 2015 23:09:02 +0000 Subject: [PATCH 28/33] Some map fixes. --- src/riak_dt_map.erl | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 8d0287e..1fc1bd1 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -356,7 +356,7 @@ get_entry({_Name, Type}=Field, Fields, Clock) -> apply_ops([], _Dot, Map, _Ctx) -> {ok, Map}; apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> - {_, CRDT} = get_entry(Field, Values, Clock), + {{_,_,CRDTDef}, CRDT} = get_entry(Field, Values, Clock), case Type:update(Op, Dot, CRDT, Ctx) of {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), @@ -366,7 +366,15 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer error -> ?DICT:store(Field, {{[Dot], [], ?FRESH_CLOCK}, Updated}, Values) end, - apply_ops(Rest, Dot, {Clock, NewValues, Deferred}, Ctx); + %% Propagate previous remove operations that were tombstoned. + %% Code could be better. + NewValues1 = case length(CRDTDef) > 0 of + true -> + {_, Out} = propagate_remove(Field, NewValues, Clock, Ctx), + Out; + _ -> NewValues + end, + apply_ops(Rest, Dot, {Clock, NewValues1, Deferred}, Ctx); Error -> Error end; @@ -400,7 +408,7 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> {ok, {Clock, ?DICT:erase(Field, Values), Deferred}} end; -remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> +remove_field({_,Type}=Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), {DefCtx, UpdtValues} = propagate_remove(Field, Values, Clock, Ctx), NewValues = case ?DICT:find(Field, UpdtValues) of @@ -419,7 +427,11 @@ remove_field(Field, {Clock, Values, Deferred0}, Ctx) -> {ok, CRDT} -> ?DICT:store(Field, CRDT, UpdtValues); error -> - UpdtValues + case riak_dt_vclock:descends(Clock, Ctx) of + true -> UpdtValues; + false -> + ?DICT:store(Field, {{?FRESH_CLOCK, [], Ctx},Type:new()}, UpdtValues) + end end, {ok, {Clock, NewValues, Deferred}}. @@ -451,6 +463,8 @@ ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. -spec propagate_remove(field_name(), entries() | field_value(), riak_dt_vclock:vclock(), riak_dt:context()) -> {riak_dt_vclock:vclock(), entries() | empty}. +propagate_remove(_Type, Entry, _MapClock, undefined)-> {[],Entry}; + propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred}}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> @@ -528,7 +542,11 @@ defer_remove(Clock, Ctx, Field, Deferred) -> Deferred) end. -spec merge(map(), map()) -> map(). -merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) -> +merge({LHSClock0, LHSEntries0, LHSDeferred0}, {RHSClock0, RHSEntries0, RHSDeferred0}) -> + %% Clear entries that do not use dots to identify updates + {LHSClock, LHSEntries, LHSDeferred} = apply_deferred(LHSClock0, LHSEntries0, RHSDeferred0), + {RHSClock, RHSEntries, RHSDeferred} = apply_deferred(RHSClock0, RHSEntries0, LHSDeferred0), + Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), Fields = lists:umerge(?DICT:fetch_keys(LHSEntries), ?DICT:fetch_keys(RHSEntries)), Entries = lists:foldl(fun({_Name, Type}=Field, Acc) -> @@ -549,6 +567,8 @@ merge({LHSClock, LHSEntries, LHSDeferred}, {RHSClock, RHSEntries, RHSDeferred}) Fields), Deferred = merge_deferred(LHSDeferred, RHSDeferred), CRDT = apply_deferred(Clock, Entries, Deferred), + %% What should be done first: clear tombstones or apply deferred? + %% Can we remove this apply deferred? clear_tombstones(CRDT). -spec keep_dots(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), riak_dt_vclock:vclock()) -> riak_dt_vclock:vclock(). @@ -656,7 +676,9 @@ pairwise_equals([{{Name, Type}, {{Dots1, S1, Tombstone1}, CRDT1}}|Rest1], [{{Nam pairwise_equals(Rest1, Rest2); _ -> false - end. + end; +pairwise_equals(_, _) -> false. + %% @doc an opaque context that can be passed to `update/4' to ensure %% that only seen fields are removed. If a field removal operation has From daf9d604ad886b44080d53a6c142892f8a4cd15e Mon Sep 17 00:00:00 2001 From: balegas Date: Sun, 15 Feb 2015 23:55:21 +0000 Subject: [PATCH 29/33] WIP - Quickcheck Passing; needs cleanup. --- src/riak_dt_map.erl | 85 +++++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 1fc1bd1..2a03702 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -368,8 +368,8 @@ apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Defer end, %% Propagate previous remove operations that were tombstoned. %% Code could be better. - NewValues1 = case length(CRDTDef) > 0 of - true -> + NewValues1 = case Type of + riak_dt_map when length(CRDTDef) > 0 -> {_, Out} = propagate_remove(Field, NewValues, Clock, Ctx), Out; _ -> NewValues @@ -404,11 +404,27 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> case ?DICT:find(Field, Values) of error -> {error, {precondition, {not_present, Field}}}; + % {ok, _Removed} -> + % {ok, {Clock, + % dict:fold(fun({_,T}=K, {{D,S,Tomb},_CRDT}=A, Acc) -> + % case riak_dt_vclock:descends(Clock, Tomb) of + % false -> ?DICT:store(K, {{D,S,Tomb},T:new()},Acc); + % true -> ?DICT:store(K,A,Acc) + % end + % end, ?DICT:new(), Values), + % + % dict:fold(fun(K, V, Acc) -> + % S = ordsets:del_element(Field, V), + % case S of + %[] -> Acc; + % _ -> ?DICT:store(K, S, Acc) + % end + % end, ?DICT:new(), Deferred)}} {ok, _Removed} -> {ok, {Clock, ?DICT:erase(Field, Values), Deferred}} end; -remove_field({_,Type}=Field, {Clock, Values, Deferred0}, Ctx) -> +remove_field({_,_Type}=Field, {Clock, Values, Deferred0}, Ctx) -> Deferred = defer_remove(Clock, Ctx, Field, Deferred0), {DefCtx, UpdtValues} = propagate_remove(Field, Values, Clock, Ctx), NewValues = case ?DICT:find(Field, UpdtValues) of @@ -419,7 +435,8 @@ remove_field({_,Type}=Field, {Clock, Values, Deferred0}, Ctx) -> {{Dots, _S, Tombstone}, CRDT} -> Tombstone = riak_dt_vclock:merge([DefCtx, Tombstone]), {{Dots, _S, Tombstone}, CRDT}; - error -> UpdtValues + error -> + UpdtValues end end,UpdtValues); {ok, empty} -> @@ -427,11 +444,13 @@ remove_field({_,Type}=Field, {Clock, Values, Deferred0}, Ctx) -> {ok, CRDT} -> ?DICT:store(Field, CRDT, UpdtValues); error -> - case riak_dt_vclock:descends(Clock, Ctx) of - true -> UpdtValues; - false -> - ?DICT:store(Field, {{?FRESH_CLOCK, [], Ctx},Type:new()}, UpdtValues) - end + %% Store entry on find error if map hasn't seen the context + %case riak_dt_vclock:descends(Clock, Ctx) of + % true -> UpdtValues; + % false -> + % ?DICT:store(Field, {{?FRESH_CLOCK, [], Ctx},Type:new()}, UpdtValues) + %end + UpdtValues end, {ok, {Clock, NewValues, Deferred}}. @@ -478,29 +497,31 @@ propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Defer ?DICT:store(K, Value, UpdtEntries)} end end, {?FRESH_CLOCK, ?DICT:new()}, Value0), + %_TombstoneDominated = riak_dt_vclock:descends(Clock, Tombstone), %Clear map if all entries are empty case ?DICT:size(SubEntries) of - 0 -> + 0 when length(Deferred) == 0 -> {SubMergedDef, empty}; - _ -> {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef, Tombstone])}, {Clock, SubEntries, Deferred}}} + _ -> + {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef, Tombstone])}, {Clock, SubEntries, Deferred}}} end; %% Value is a leaf: %% Merge deferred operations' context with Value clock (Tombstone) and send it upstream propagate_remove({_, Type}=Field, {{Dots, _S, TombstoneIn}, CRDT}, MapClock, Ctx) -> case Type:get_deferred(CRDT) of - [] -> - {[], ctx_rem_field(Field, {{Dots, _S, []}, CRDT}, Ctx, MapClock)}; + [] when length(Dots) > 0 -> + {[], ctx_rem_field(Field, {{Dots, _S, TombstoneIn}, CRDT}, Ctx, MapClock)}; Deferred -> Intersection = riak_dt_vclock:glb(MapClock,Ctx), Tombstone = riak_dt_vclock:merge([Intersection, TombstoneIn | Deferred]), %% Clear CRDT - TombstoneClock = riak_dt_vclock:glb(MapClock, Ctx), - TS = Type:parent_clock(TombstoneClock, Type:new()), - ClearedCRDT = Type:merge(TS, Type:parent_clock(TombstoneClock, CRDT)), + TombstoneClock = riak_dt_vclock:glb(MapClock, Ctx), + TS = Type:parent_clock(TombstoneClock, Type:new()), + ClearedCRDT = Type:merge(TS, Type:parent_clock(TombstoneClock, CRDT)), - {Tombstone, {{Dots, _S, Tombstone}, ClearedCRDT}} + {Tombstone, {{Dots, _S, Tombstone}, Type:parent_clock(?FRESH_CLOCK, ClearedCRDT)}} end; propagate_remove(Field, Values, MapClock, Ctx) -> @@ -565,6 +586,7 @@ merge({LHSClock0, LHSEntries0, LHSDeferred0}, {RHSClock0, RHSEntries0, RHSDeferr end, ?DICT:new(), Fields), + Deferred = merge_deferred(LHSDeferred, RHSDeferred), CRDT = apply_deferred(Clock, Entries, Deferred), %% What should be done first: clear tombstones or apply deferred? @@ -594,21 +616,30 @@ merge_deferred(LHS, RHS) -> {riak_dt_vclock:vclock(), entries(), deferred()}. apply_deferred(Clock, Entries, Deferred) -> ?DICT:fold(fun(Ctx, Fields, Map) -> - remove_all(Fields, Map, Ctx) + lists:foldl(fun(Field, {Ci,Ei,Defi}=Mapi) -> + case ?DICT:is_key(Field, Ei) of + true -> + {ok, Res} = remove_field(Field, Mapi, Ctx), + Res; + false -> + Def = defer_remove(Clock, Ctx, Field, Defi), + {Ci,Ei,Def} + end + end, Map, Fields) end, {Clock, Entries, ?DICT:new()}, Deferred). %% @private --spec remove_all([field_name()], map(), riak_dt:context()) -> - map(). -remove_all(Fields, Map, Ctx) -> - lists:foldl(fun(Field, MapAcc) -> - {ok, MapAcc2}= remove_field(Field, MapAcc, Ctx), - MapAcc2 - end, - Map, - Fields). +%-spec remove_all([field_name()], map(), riak_dt:context()) -> +% map(). +%remove_all(Fields, Map, Ctx) -> +% lists:foldl(fun(Field, MapAcc) -> +% {ok, MapAcc2}= remove_field(Field, MapAcc, Ctx), +% MapAcc2 +% end, +% Map, +% Fields). %% @private %%Eliminates the tombstone if it has been integrated in the object's clock. -spec clear_tombstones(map()) -> map(). From e7af48224ce33015cdc2a6b7c61a2f28f13d95bd Mon Sep 17 00:00:00 2001 From: balegas Date: Wed, 18 Feb 2015 16:49:33 +0000 Subject: [PATCH 30/33] More QuickCheck Fixes. --- src/riak_dt_map.erl | 140 ++++++++++++++++---------------------------- 1 file changed, 51 insertions(+), 89 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 2a03702..615f7a3 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -275,16 +275,16 @@ value({Clock, Values, _Deferred}) -> case SubTree of [] -> Acc; - _ -> [{{Name, Type}, Type:value(CRDT)} | Acc] - end; - _ -> - [{{Name, Type}, Type:value(CRDT)} | Acc] - end - end - end - end, - [], - Values)). + _ -> [{{Name, Type}, Type:value(CRDT)} | Acc] + end; + _ -> + [{{Name, Type}, Type:value(CRDT)} | Acc] + end + end + end + end, + [], + Values)). %% @doc query map (not implemented yet) %% @@ -342,11 +342,11 @@ update_clock(Actor, Clock) -> field_value(). get_entry({_Name, Type}=Field, Fields, Clock) -> {{Dots, _S, Tombstone}, CRDT} = case ?DICT:find(Field, Fields) of - {ok, Entry} -> - Entry; - error -> - {{[], [], ?FRESH_CLOCK}, Type:new()} - end, + {ok, Entry} -> + Entry; + error -> + {{[], [], ?FRESH_CLOCK}, Type:new()} + end, {{Dots, _S, Tombstone}, Type:parent_clock(Clock, CRDT)}. %% @private @@ -355,26 +355,16 @@ get_entry({_Name, Type}=Field, Fields, Clock) -> {ok, map()} | precondition_error(). apply_ops([], _Dot, Map, _Ctx) -> {ok, Map}; -apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}, Ctx) -> - {{_,_,CRDTDef}, CRDT} = get_entry(Field, Values, Clock), +apply_ops([{update, {_Name, Type}=Field, Op} | Rest], Dot, {Clock, Values, Deferred}=_ALL, Ctx) -> + {{_,_, Tombstone}, CRDT} = get_entry(Field, Values, Clock), case Type:update(Op, Dot, CRDT, Ctx) of {ok, Updated0} -> Updated = Type:parent_clock(?FRESH_CLOCK, Updated0), - NewValues = case ?DICT:find(Field,Values) of - {ok, {{_,_,Tombstone}, _}} -> - ?DICT:store(Field, {{[Dot], [], Tombstone}, Updated}, Values); - error -> - ?DICT:store(Field, {{[Dot], [], ?FRESH_CLOCK}, Updated}, Values) - end, + NewValues = ?DICT:store(Field, {{[Dot], [], Tombstone}, Updated}, Values), %% Propagate previous remove operations that were tombstoned. - %% Code could be better. - NewValues1 = case Type of - riak_dt_map when length(CRDTDef) > 0 -> - {_, Out} = propagate_remove(Field, NewValues, Clock, Ctx), - Out; - _ -> NewValues - end, - apply_ops(Rest, Dot, {Clock, NewValues1, Deferred}, Ctx); + %% This is expensive. + UpdtCRDT = apply_deferred({Clock, NewValues, Deferred}), + apply_ops(Rest, Dot, UpdtCRDT, Ctx); Error -> Error end; @@ -404,22 +394,6 @@ remove_field(Field, {Clock, Values, Deferred}, undefined) -> case ?DICT:find(Field, Values) of error -> {error, {precondition, {not_present, Field}}}; - % {ok, _Removed} -> - % {ok, {Clock, - % dict:fold(fun({_,T}=K, {{D,S,Tomb},_CRDT}=A, Acc) -> - % case riak_dt_vclock:descends(Clock, Tomb) of - % false -> ?DICT:store(K, {{D,S,Tomb},T:new()},Acc); - % true -> ?DICT:store(K,A,Acc) - % end - % end, ?DICT:new(), Values), - % - % dict:fold(fun(K, V, Acc) -> - % S = ordsets:del_element(Field, V), - % case S of - %[] -> Acc; - % _ -> ?DICT:store(K, S, Acc) - % end - % end, ?DICT:new(), Deferred)}} {ok, _Removed} -> {ok, {Clock, ?DICT:erase(Field, Values), Deferred}} end; @@ -444,12 +418,6 @@ remove_field({_,_Type}=Field, {Clock, Values, Deferred0}, Ctx) -> {ok, CRDT} -> ?DICT:store(Field, CRDT, UpdtValues); error -> - %% Store entry on find error if map hasn't seen the context - %case riak_dt_vclock:descends(Clock, Ctx) of - % true -> UpdtValues; - % false -> - % ?DICT:store(Field, {{?FRESH_CLOCK, [], Ctx},Type:new()}, UpdtValues) - %end UpdtValues end, {ok, {Clock, NewValues, Deferred}}. @@ -482,25 +450,28 @@ ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. -spec propagate_remove(field_name(), entries() | field_value(), riak_dt_vclock:vclock(), riak_dt:context()) -> {riak_dt_vclock:vclock(), entries() | empty}. -propagate_remove(_Type, Entry, _MapClock, undefined)-> {[],Entry}; +propagate_remove(_Type, Entry, _MapClock, undefined)-> {[], Entry}; -propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred}}, MapClock, Ctx)-> +propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred} = CRDT}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> case propagate_remove(K, V, MapClock, Ctx) of - {_, empty} -> - {UpdtClock, UpdtEntries}; - {TombstoneClock, Value} -> - %%Some deferred operation in subtree - %% - keep entry, update tombstones - {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), - ?DICT:store(K, Value, UpdtEntries)} - end - end, {?FRESH_CLOCK, ?DICT:new()}, Value0), - %_TombstoneDominated = riak_dt_vclock:descends(Clock, Tombstone), + {_, empty} -> + {UpdtClock, UpdtEntries}; + {TombstoneClock, Value} -> + %%Some deferred operation in subtree + %% - keep entry, update tombstones + {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), + ?DICT:store(K, Value, UpdtEntries)} + end + end, {?FRESH_CLOCK, ?DICT:new()}, Value0), + UncoveredDeferred = lists:filter(fun(DefOp) -> + riak_dt_vclock:descends(Clock, DefOp) + end, get_deferred(CRDT)), + Descends = riak_dt_vclock:descends(Ctx, MapClock), %Clear map if all entries are empty case ?DICT:size(SubEntries) of - 0 when length(Deferred) == 0 -> + 0 when length(UncoveredDeferred) == 0, Descends -> {SubMergedDef, empty}; _ -> {SubMergedDef, {{Dots, _S, riak_dt_vclock:merge([SubMergedDef, Tombstone])}, {Clock, SubEntries, Deferred}}} @@ -551,22 +522,22 @@ propagate_remove(Field, Values, MapClock, Ctx) -> %% causal delivery, in that an `update' must be seen before it can be %% `removed'. -spec defer_remove(riak_dt_vclock:vclock(), riak_dt_vclock:vclock(), field_name(), deferred()) -> - deferred(). + deferred(). defer_remove(Clock, Ctx, Field, Deferred) -> case riak_dt_vclock:descends(Clock, Ctx) of %% no need to save this remove, we're done true -> Deferred; false -> ?DICT:update(Ctx, - fun(Fields) -> - ordsets:add_element(Field, Fields) end, - ordsets:add_element(Field, ordsets:new()), - Deferred) + fun(Fields) -> + ordsets:add_element(Field, Fields) end, + ordsets:add_element(Field, ordsets:new()), + Deferred) end. -spec merge(map(), map()) -> map(). merge({LHSClock0, LHSEntries0, LHSDeferred0}, {RHSClock0, RHSEntries0, RHSDeferred0}) -> %% Clear entries that do not use dots to identify updates - {LHSClock, LHSEntries, LHSDeferred} = apply_deferred(LHSClock0, LHSEntries0, RHSDeferred0), - {RHSClock, RHSEntries, RHSDeferred} = apply_deferred(RHSClock0, RHSEntries0, LHSDeferred0), + {LHSClock, LHSEntries, LHSDeferred} = apply_deferred({LHSClock0, LHSEntries0, RHSDeferred0}), + {RHSClock, RHSEntries, RHSDeferred} = apply_deferred({RHSClock0, RHSEntries0, LHSDeferred0}), Clock = riak_dt_vclock:merge([LHSClock, RHSClock]), Fields = lists:umerge(?DICT:fetch_keys(LHSEntries), ?DICT:fetch_keys(RHSEntries)), @@ -588,7 +559,7 @@ merge({LHSClock0, LHSEntries0, LHSDeferred0}, {RHSClock0, RHSEntries0, RHSDeferr Fields), Deferred = merge_deferred(LHSDeferred, RHSDeferred), - CRDT = apply_deferred(Clock, Entries, Deferred), + CRDT = apply_deferred({Clock, Entries, Deferred}), %% What should be done first: clear tombstones or apply deferred? %% Can we remove this apply deferred? clear_tombstones(CRDT). @@ -607,14 +578,13 @@ keep_dots(LHSDots, RHSDots, LHSClock, RHSClock) -> -spec merge_deferred(deferred(), deferred()) -> deferred(). merge_deferred(LHS, RHS) -> ?DICT:merge(fun(_K, LH, RH) -> - ordsets:union(LH, RH) end, - LHS, RHS). + ordsets:union(LH, RH) end, + LHS, RHS). %% @private apply those deferred field removals, if they're %% preconditions have been met, that is. --spec apply_deferred(riak_dt_vclock:vclock(), entries(), deferred()) -> - {riak_dt_vclock:vclock(), entries(), deferred()}. -apply_deferred(Clock, Entries, Deferred) -> +-spec apply_deferred(map()) -> map(). +apply_deferred({Clock, Entries, Deferred}) -> ?DICT:fold(fun(Ctx, Fields, Map) -> lists:foldl(fun(Field, {Ci,Ei,Defi}=Mapi) -> case ?DICT:is_key(Field, Ei) of @@ -622,6 +592,8 @@ apply_deferred(Clock, Entries, Deferred) -> {ok, Res} = remove_field(Field, Mapi, Ctx), Res; false -> + %%If there is no key, applying the deferred operation + %%would not affect the state of the object. Def = defer_remove(Clock, Ctx, Field, Defi), {Ci,Ei,Def} end @@ -630,16 +602,6 @@ apply_deferred(Clock, Entries, Deferred) -> {Clock, Entries, ?DICT:new()}, Deferred). -%% @private -%-spec remove_all([field_name()], map(), riak_dt:context()) -> -% map(). -%remove_all(Fields, Map, Ctx) -> -% lists:foldl(fun(Field, MapAcc) -> -% {ok, MapAcc2}= remove_field(Field, MapAcc, Ctx), -% MapAcc2 -% end, -% Map, -% Fields). %% @private %%Eliminates the tombstone if it has been integrated in the object's clock. -spec clear_tombstones(map()) -> map(). From afb03a96b5333520174dfb340539fd192d2f96bf Mon Sep 17 00:00:00 2001 From: balegas Date: Wed, 18 Feb 2015 18:58:00 +0000 Subject: [PATCH 31/33] Code indentation. --- src/riak_dt_map.erl | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 615f7a3..1e24530 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -621,8 +621,8 @@ clear_tombstones_handle({_, riak_dt_map}=Field, {{Dots, _S, Tombstone}, {Clock, TombstoneCovered = riak_dt_vclock:descends(MapClock, Tombstone), case ?DICT:size(FilteredEntries) == 0 of true when length(Tombstone) > 0 andalso TombstoneCovered -> - % No childs, a tombstone was set (remove executed) and the clock dominates tombstone - NewMap; + % No childs, a tombstone was set (remove executed) and the clock dominates tombstone + NewMap; _ -> case TombstoneCovered of true -> @@ -656,9 +656,9 @@ clear_tombstones_handle(Field, {{Dots, _S, Tombstone}, CRDT}=Value, NewMap, MapC -spec equal(map(), map()) -> boolean(). equal({Clock1, Values1, Deferred1}, {Clock2, Values2, Deferred2}) -> riak_dt_vclock:equal(Clock1, Clock2) andalso - Deferred1 == Deferred2 andalso - pairwise_equals(lists:sort(?DICT:to_list(Values1)), - lists:sort(?DICT:to_list(Values2))). + Deferred1 == Deferred2 andalso + pairwise_equals(lists:sort(?DICT:to_list(Values1)), + lists:sort(?DICT:to_list(Values2))). -spec pairwise_equals(entries() | [], entries() | []) -> boolean(). pairwise_equals([], []) -> @@ -733,9 +733,9 @@ to_binary(?V1_VERS, Map0) -> %% @private transpose a v1 map (orddicts) to a v2 (dicts) -spec to_v2({riak_dt_vclock:vclock(), orddict:orddict() | dict(), orddict:orddict() | dict()}) -> - {riak_dt_vclock:vclock(), dict(), dict()}. + {riak_dt_vclock:vclock(), dict(), dict()}. to_v2({Clock, Fields0, Deferred0}) when is_list(Fields0), - is_list(Deferred0) -> + is_list(Deferred0) -> Fields = ?DICT:from_list(Fields0), Deferred = ?DICT:from_list(Deferred0), {Clock, Fields, Deferred}; @@ -745,9 +745,9 @@ to_v2(S) -> %% @private transpose a v2 map (dicts) to a v1 (orddicts) -spec to_v1({riak_dt_vclock:vclock(), orddict:orddict() | dict(), orddict:orddict() | dict()}) -> - {riak_dt_vclock:vclock(), orddict:orddict(), orddict:orddict()}. + {riak_dt_vclock:vclock(), orddict:orddict(), orddict:orddict()}. to_v1({_Clock, Fields0, Deferred0}=S) when is_list(Fields0), - is_list(Deferred0) -> + is_list(Deferred0) -> S; to_v1({Clock, Fields0, Deferred0}) -> %% Must be dicts, there is no is_dict test though @@ -974,8 +974,8 @@ clear_invisible_after_merge_set_2_test() -> {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {add, Elem}}]}}]} end, RemElemFromS = fun(Elem) -> - {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {remove, Elem}}]}}]} - end, + {update, [{update, ?FIELD, {update, [{update, ?FIELD_S, {remove, Elem}}]}}]} + end, InitialState = new(), {ok, {CtxA1,_,_}=StateA1} = update(AddElemToS(0), a, InitialState), {ok, {CtxB1,_,_}=StateB1} = update(RemElemFromS(0), b, InitialState, CtxA1), @@ -1184,7 +1184,7 @@ size(Map) -> byte_size(term_to_binary(Map)) div 10. generate() -> - ?LET({Ops, Actors}, {non_empty(list(gen_op())), non_empty(list(bitstring(16*8)))}, + ?LET({Ops, Actors}, {non_empty(list(gen_op())), non_empty(list(bitstring(16*8)))}, lists:foldl(fun(Op, Map) -> Actor = case length(Actors) of 1 -> hd(Actors); @@ -1215,12 +1215,12 @@ gen_field() -> gen_field(Size) -> {growingelements(['A', 'B', 'C', 'X', 'Y', 'Z']) %% Macro? Bigger? - , elements([ - riak_dt_emcntr, - riak_dt_orswot, -%% riak_dt_lwwreg, - riak_dt_od_flag - ] ++ [?MODULE || Size > 0])}. + , elements([ + riak_dt_emcntr, + riak_dt_orswot, + %% riak_dt_lwwreg, + riak_dt_od_flag + ] ++ [?MODULE || Size > 0])}. gen_field_op({_Name, Type}, Size) -> Type:gen_op(Size). From 3bddd00fda9579cfd2cd5c7d4b86c4268e2bd1d6 Mon Sep 17 00:00:00 2001 From: Russell Brown Date: Mon, 23 Feb 2015 11:44:46 +0000 Subject: [PATCH 32/33] When running EQC, use an orddict for easier to read output --- src/riak_dt_map.erl | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 615f7a3..3f74e1b 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -228,11 +228,18 @@ riak_dt_map:map_op() | riak_dt_map:map_op(). -type value() :: {field_name(), riak_dt_map:values() | integer() | [term()] | boolean() | term()}. -type precondition_error() :: {error, {precondition, {not_present, field_name()}}}. -%% used until we move to erlang 17 and can use dict:dict/2 --type dict(_A, _B) :: dict(). -define(FRESH_CLOCK, riak_dt_vclock:fresh()). + +-ifdef(EQC). +-define(DICT, orddict). +-type dict(_A, _B) :: orddict:orddict(). + +-else. +%% used until we move to erlang 17 and can use dict:dict/2 +-type dict(_A, _B) :: dict(). -define(DICT, dict). +-endif. %% @doc Create a new, empty Map. -spec new() -> map(). @@ -449,27 +456,26 @@ ctx_rem_field({_, Type}, {{Dots, _S, Tombstone}, CRDT}, Ctx, MapClock) -> %% Value is a map: %% Remove fields that don't have deferred operations; %% Compute the removal tombstone for this field. --spec propagate_remove(field_name(), entries() | field_value(), riak_dt_vclock:vclock(), riak_dt:context()) -> {riak_dt_vclock:vclock(), entries() | empty}. -propagate_remove(_Type, Entry, _MapClock, undefined)-> {[], Entry}; - +-spec propagate_remove(field_name(), entries() | field_value(), riak_dt_vclock:vclock(), riak_dt:context()) -> + {riak_dt_vclock:vclock(), entries() | empty}. propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Deferred} = CRDT}, MapClock, Ctx)-> {SubMergedDef, SubEntries} = - ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> - case propagate_remove(K, V, MapClock, Ctx) of - {_, empty} -> - {UpdtClock, UpdtEntries}; - {TombstoneClock, Value} -> - %%Some deferred operation in subtree - %% - keep entry, update tombstones - {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), - ?DICT:store(K, Value, UpdtEntries)} - end - end, {?FRESH_CLOCK, ?DICT:new()}, Value0), + ?DICT:fold(fun(K, V, {UpdtClock, UpdtEntries}) -> + case propagate_remove(K, V, MapClock, Ctx) of + {_, empty} -> + {UpdtClock, UpdtEntries}; + {TombstoneClock, Value} -> + %%Some deferred operation in subtree + %% - keep entry, update tombstones + {riak_dt_vclock:merge([TombstoneClock, UpdtClock]), + ?DICT:store(K, Value, UpdtEntries)} + end + end, {?FRESH_CLOCK, ?DICT:new()}, Value0), UncoveredDeferred = lists:filter(fun(DefOp) -> riak_dt_vclock:descends(Clock, DefOp) end, get_deferred(CRDT)), Descends = riak_dt_vclock:descends(Ctx, MapClock), - %Clear map if all entries are empty + %Clear map if all entries are empty case ?DICT:size(SubEntries) of 0 when length(UncoveredDeferred) == 0, Descends -> {SubMergedDef, empty}; From 54ab49d2fb9eecfdf390f8485c5dfbf6033a3b56 Mon Sep 17 00:00:00 2001 From: balegas Date: Mon, 23 Feb 2015 16:50:16 +0000 Subject: [PATCH 33/33] Fix another quick check error --- src/riak_dt_map.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riak_dt_map.erl b/src/riak_dt_map.erl index 6ee9690..112e9b9 100644 --- a/src/riak_dt_map.erl +++ b/src/riak_dt_map.erl @@ -474,7 +474,7 @@ propagate_remove({_, riak_dt_map}, {{Dots, _S, Tombstone}, {Clock, Value0, Defer UncoveredDeferred = lists:filter(fun(DefOp) -> riak_dt_vclock:descends(Clock, DefOp) end, get_deferred(CRDT)), - Descends = riak_dt_vclock:descends(Ctx, MapClock), + Descends = riak_dt_vclock:descends(Ctx, Dots), %Clear map if all entries are empty case ?DICT:size(SubEntries) of 0 when length(UncoveredDeferred) == 0, Descends ->