├── rebar
├── .gitignore
├── .travis.yml
├── Makefile
├── src
├── statebox.app.src
├── statebox_identity.erl
├── statebox_clock.erl
├── statebox_counter.erl
├── statebox_orddict.erl
└── statebox.erl
├── rebar.config
├── test
├── statebox_identity_tests.erl
└── statebox_counter_tests.erl
├── CHANGES.md
├── LICENSE
└── README.md
/rebar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mochi/statebox/HEAD/rebar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /ebin
2 | /doc
3 | /_test
4 | /.eunit
5 | /docs
6 | .DS_Store
7 | /TEST-*.xml
8 | /deps
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: erlang
2 | notifications:
3 | email: false
4 | otp_release:
5 | - R15B03
6 | - R16B01
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REBAR ?= $(shell which rebar 2>/dev/null || which ./rebar)
2 |
3 | .PHONY: all edoc test clean
4 |
5 | all:
6 | @$(REBAR) get-deps compile
7 |
8 | edoc:
9 | @$(REBAR) doc skip_deps=true
10 |
11 | test:
12 | @$(REBAR) skip_deps=true eunit
13 |
14 | clean:
15 | @$(REBAR) clean
16 |
--------------------------------------------------------------------------------
/src/statebox.app.src:
--------------------------------------------------------------------------------
1 | {application, statebox,
2 | [
3 | {description, "Erlang state \"monad\" with merge/conflict-resolution capabilities. Useful for Riak."},
4 | {vsn, "0.2.5"},
5 | {registered, []},
6 | {applications, [
7 | kernel,
8 | stdlib
9 | ]},
10 | {env, []}
11 | ]}.
12 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %% -*- erlang -*-
2 | {erl_opts, [fail_on_warning,
3 | debug_info]}.
4 | {deps,
5 | [{meck, ".*",
6 | {git, "git://github.com/eproxus/meck.git", "master"}},
7 | {proper, ".*",
8 | {git, "git://github.com/manopapad/proper", "master"}}]}.
9 | {cover_enabled, true}.
10 | {clean_files, ["*.eunit", "ebin/*.beam"]}.
11 | {eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}.
12 |
--------------------------------------------------------------------------------
/test/statebox_identity_tests.erl:
--------------------------------------------------------------------------------
1 | -module(statebox_identity_tests).
2 | -include_lib("eunit/include/eunit.hrl").
3 |
4 | entropy_unique_test() ->
5 | ?assertNot(
6 | statebox_identity:entropy() =:= statebox_identity:entropy()).
7 |
8 | entropy_idempotent_test() ->
9 | NP = {Node, Now} = {'node', {1, 2, 3}},
10 | ?assertEqual(
11 | erlang:phash2(NP),
12 | statebox_identity:entropy(Node, Now)),
13 | ?assertEqual(
14 | statebox_identity:entropy(Node, Now),
15 | statebox_identity:entropy(Node, Now)).
16 |
--------------------------------------------------------------------------------
/src/statebox_identity.erl:
--------------------------------------------------------------------------------
1 | %% @doc Functions for uniquely identifying events.
2 | -module(statebox_identity).
3 |
4 | -export([entropy/2, entropy/0]).
5 |
6 | -type entropy() :: 1..4294967296.
7 | -export_type([entropy/0]).
8 |
9 | %% @equiv entropy(node(), statebox_clock:now())
10 | -spec entropy() -> entropy().
11 | entropy() ->
12 | entropy(node(), statebox_clock:now()).
13 |
14 | %% @doc Return an integer that can be expected to be reasonably unique
15 | %% at a given msec timestamp.
16 | -spec entropy(node(), statebox_clock:t_now()) -> entropy().
17 | entropy(Node, Now) ->
18 | erlang:phash2({Node, Now}).
19 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | Version 0.2.4 released 2013-10-24
2 |
3 | * Cleaned up type specs for R15B dialyzer
4 | https://github.com/mochi/statebox/issues/6
5 |
6 | Version 0.2.3 released 2013-09-15
7 |
8 | * Cleaned up type specs for dialyzer
9 | https://github.com/mochi/statebox/issues/4
10 |
11 | Version 0.2.2 released 2011-06-14
12 |
13 | * Updated README
14 | * statebox_orddict:orddict_from_proplist/1 now uses
15 | orddict:from_list/1 instead of lists:usort/1 for correctness
16 | * Fixed docstring for statebox_orddict:f_subtract/2
17 |
18 | Version 0.2.1 released 2011-04-30
19 |
20 | * Added LICENSE file (MIT)
21 |
22 | Version 0.2.0 released 2011-04-22
23 |
24 | * Added statebox_orddict convenience wrapper
25 |
26 | Version 0.1.0 released 2011-04-23
27 |
28 | * Added batch operation support
29 | * Added is_statebox/1 function
30 | * Initial release
31 |
--------------------------------------------------------------------------------
/src/statebox_clock.erl:
--------------------------------------------------------------------------------
1 | %% @doc Timestamp functions for statebox:new/1 and
2 | %% statebox:modify/2.
3 |
4 | -module(statebox_clock).
5 | -export([timestamp/0, now_to_msec/1, now/0]).
6 |
7 | -type t_now() :: {integer(), integer(), integer()}.
8 | -type timestamp() :: integer().
9 | -export_type([t_now/0, timestamp/0]).
10 |
11 | -define(KILO, 1000).
12 | -define(MEGA, 1000000).
13 |
14 | %% @doc Current UNIX epoch timestamp in integer milliseconds.
15 | %% Equivalient to now_to_msec(os:timestamp()).
16 | -spec timestamp() -> timestamp().
17 | timestamp() ->
18 | now_to_msec(os:timestamp()).
19 |
20 | %% @doc Converts given time of now() format to UNIX epoch timestamp in
21 | %% integer milliseconds.
22 | -spec now_to_msec(t_now()) -> timestamp().
23 | now_to_msec({MegaSecs, Secs, MicroSecs}) ->
24 | trunc(((MegaSecs * ?MEGA) + Secs + (MicroSecs / ?MEGA)) * ?KILO).
25 |
26 | -spec now() -> t_now().
27 | now() ->
28 | erlang:now().
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is the MIT license.
2 |
3 | Copyright (c) 2011 Mochi Media, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/src/statebox_counter.erl:
--------------------------------------------------------------------------------
1 | %% @doc Integer counter based on an ordered list of counter events.
2 | %%
3 | %% A counter is stored as an orddict of counter events. Each counter
4 | %% event has a unique key based on the timestamp and some entropy, and it
5 | %% stores the delta from the inc operation. The value of a counter is the
6 | %% sum of all these deltas.
7 | %%
8 | %% As an optimization, counter events older than a given age are coalesced
9 | %% to a single counter event with a key in the form of
10 | %% {timestamp(), 'acc'}.
11 |
12 | -module(statebox_counter).
13 | -export([value/1, merge/1, accumulate/2, inc/3]).
14 | -export([f_inc_acc/2, f_inc_acc/3, op_inc_acc/4]).
15 |
16 | -type op() :: statebox:op().
17 | -type timestamp() :: statebox_clock:timestamp().
18 | -type timedelta() :: statebox:timedelta().
19 | -type counter_id() :: statebox_identity:entropy() | acc.
20 | -type counter_key() :: {timestamp(), counter_id()}.
21 | -type counter_op() :: {counter_key(), integer()}.
22 | -type counter() :: [counter_op()].
23 |
24 | %% @doc Return the value of the counter (the sum of all counter event deltas).
25 | -spec value(counter()) -> integer().
26 | value([]) ->
27 | 0;
28 | value([{_Key, Value} | Rest]) ->
29 | Value + value(Rest).
30 |
31 | %% @doc Merge the given list of counters and return a new counter
32 | %% with the union of that history.
33 | -spec merge([counter()]) -> counter().
34 | merge([Counter]) ->
35 | Counter;
36 | merge(Counters) ->
37 | orddict:from_list(merge_prune(Counters)).
38 |
39 | %% @doc Accumulate all counter events older than Timestamp to
40 | %% the key {Timestamp, acc}. If there is already an
41 | %% acc at or before Timestamp this is a no-op.
42 | -spec accumulate(timestamp(), counter()) -> counter().
43 | accumulate(Timestamp, Counter=[{{T0, acc}, _} | _]) when Timestamp =< T0 ->
44 | Counter;
45 | accumulate(Timestamp, Counter) ->
46 | accumulate(Timestamp, Counter, 0).
47 |
48 | %% @doc Return a new counter with the given counter event. If there is
49 | %% an acc at or before the timestamp of the given key then
50 | %% this is a no-op.
51 | -spec inc(counter_key(), integer(), counter()) -> counter().
52 | inc({T1, _Id1}, _Value, Counter=[{{T0, acc}, _} | _Rest]) when T1 =< T0 ->
53 | Counter;
54 | inc(Key, Value, Counter) ->
55 | orddict:store(Key, Value, Counter).
56 |
57 | %% @equiv f_inc_acc(Value, Age, {statebox_clock:timestamp(),
58 | %% statebox_identity:entropy()})
59 | -spec f_inc_acc(integer(), timedelta()) -> op().
60 | f_inc_acc(Value, Age) ->
61 | Key = {statebox_clock:timestamp(), statebox_identity:entropy()},
62 | f_inc_acc(Value, Age, Key).
63 |
64 | %% @doc Return a statebox event to increment and accumulate the counter.
65 | %% Value is the delta,
66 | %% Age is the maximum age of counter events in milliseconds
67 | %% (this should be longer than the amount of time you expect your cluster to
68 | %% reach a consistent state),
69 | %% Key is the counter event key.
70 | -spec f_inc_acc(integer(), timedelta(), counter_key()) -> op().
71 | f_inc_acc(Value, Age, Key={Timestamp, _Id}) ->
72 | {fun ?MODULE:op_inc_acc/4, [Timestamp - Age, Key, Value]}.
73 |
74 | %% @private
75 | op_inc_acc(Timestamp, Key, Value, Counter) ->
76 | accumulate(Timestamp, inc(Key, Value, Counter)).
77 |
78 | %% Internal API
79 |
80 | merge_prune(Counters) ->
81 | %% Merge of all of the counters and prune all entries older than the
82 | %% newest {_, acc}.
83 | prune(lists:umerge(Counters)).
84 |
85 | prune(All) ->
86 | prune(All, All).
87 |
88 | prune(Here=[{{_Ts, acc}, _V} | Rest], _Last) ->
89 | prune(Rest, Here);
90 | prune([_ | Rest], Last) ->
91 | prune(Rest, Last);
92 | prune([], Last) ->
93 | Last.
94 |
95 | accumulate(Timestamp, [{{T1, _Id}, Value} | Rest], Sum) when T1 =< Timestamp ->
96 | %% Roll up old counter events
97 | accumulate(Timestamp, Rest, Value + Sum);
98 | accumulate(Timestamp, Counter, Sum) ->
99 | %% Return the new counter
100 | inc({Timestamp, acc}, Sum, Counter).
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | statebox - state "monad" for automated conflict resolution
2 | ==========================================================
3 |
4 |
5 |
6 | Overview:
7 | ---------
8 |
9 | statebox is a data structure you can use with an eventually consistent
10 | system such as riak to resolve conflicts between siblings in a deterministic
11 | manner.
12 |
13 | Status:
14 | -------
15 |
16 | Used in production at Mochi Media for multiple backend services.
17 |
18 | Theory:
19 | -------
20 |
21 | A statebox wraps a current value and an event queue. The event queue is
22 | an ordered list of `{timestamp(), op()}`. When two or more statebox
23 | are merged with `statebox:merge/1`, the event queues are merged with
24 | `lists:umerge/1` and the operations are performed again over the current
25 | value of the newest statebox, producing a new statebox with conflicts
26 | resolved in a deterministic manner.
27 |
28 | An `op()` is a `{fun(), [term()]}`, with all but the last argument specified
29 | in the term list. For example `{ordsets:add_element/2, [a]}`. To evaluate
30 | this op, `ordsets:add_element(a, value(Statebox))` will be called. It is also
31 | possible to specify an `op()` as a `{module(), atom(), [term()]}` tuple, or
32 | as a list of `op()` when performing several operations at the same timestamp.
33 |
34 | There are several important limitations on the kinds of `op()` that are safe
35 | to use (`{F, [Arg]}` is the example `op()` used below):
36 |
37 | * An `op()` must be repeatable: `F(Arg, F(Arg, Value)) =:= F(Arg, Value)`
38 | * If the `{fun(), [term()]}` form is used, the `fun()` should be a reference
39 | to an exported function.
40 | * `F(Arg, Value)` should return the same type as `Value`.
41 |
42 | Some examples of safe to use `op()` that ship with Erlang:
43 |
44 | * `{fun ordsets:add_element/2, [SomeElem]}` and
45 | `{fun ordsets:del_element/2, [SomeElem]}`
46 | * `{fun ordsets:union/2, [SomeOrdset]}` and
47 | `{fun ordsets:subtract/2, [SomeOrdset]}`
48 | * `{fun orddict:store/3, [Key, Value]}`
49 |
50 | Some examples of functions you can not use as `op()`:
51 |
52 | * `{fun orddict:update_counter, [Key, Inc]}` - it is not repeatable.
53 | `F(a, 1, [{a, 0}]) =/= F(a, 1, F(a, 1, [{a, 0}]))`
54 |
55 | Optimizations:
56 | --------------
57 |
58 | There are two functions that modify a statebox that can be used to
59 | reduce its size. One or both of these should be done every time before
60 | serializing the statebox.
61 |
62 | * `truncate(N, Statebox)` return Statebox with no more than `N` events in its
63 | queue.
64 | * `expire(Age, Statebox)` return Statebox with no events older than
65 | `last_modified(Statebox) - Age`. If using `new/1` and `modify/2`, then this
66 | is in milliseconds.
67 |
68 | Usage:
69 | ------
70 |
71 | Simple `ordsets()` example:
72 |
73 | New = statebox:new(fun () -> [] end),
74 | ChildA = statebox:modify({fun ordsets:add_element/2, [a]}, New),
75 | ChildB = statebox:modify({fun ordsets:add_element/2, [b]}, New),
76 | Resolved = statebox:merge([ChildA, ChildB]),
77 | statebox:value(Resolved) =:= [a, b].
78 |
79 | With manual control over timestamps:
80 |
81 | New = statebox:new(0, fun () -> [] end),
82 | ChildA = statebox:modify(1, {fun ordsets:add_element/2, [a]}, New),
83 | ChildB = statebox:modify(2, {fun ordsets:add_element/2, [b]}, New),
84 | Resolved = statebox:merge([ChildA, ChildB]),
85 | statebox:value(Resolved) =:= [a, b].
86 |
87 | Using the `statebox_orddict` convenience wrapper:
88 |
89 | New = statebox_orddict:from_values([]),
90 | ChildA = statebox:modify([statebox_orddict:f_store(a, 1),
91 | statebox_orddict:f_union(c, [a, aa])],
92 | New),
93 | ChildB = statebox:modify([statebox_orddict:f_store(b, 1),
94 | statebox_orddict:f_union(c, [b, bb])],
95 | New),
96 | Resovled = statebox_orddict:from_values([ChildA, ChildB]),
97 | statebox:value(Resolved) =:= [{a, 1}, {b, 1}, {c, [a, aa, b, bb]}].
98 |
99 | Resources
100 | ---------
101 |
102 | On Mochi Labs
103 | =============
104 |
105 | [statebox, an eventually consistent data model for Erlang (and Riak)][labs0]
106 | on the Mochi Labs blog describes the rationale for statebox and shows how it
107 | works.
108 |
109 | [labs0]: http://labs.mochimedia.com/archive/2011/05/08/statebox/
110 |
111 | Convergent / Commutative Replicated Data Types
112 | ==============================================
113 |
114 | The technique used to implement this is similar to what is described in
115 | this paper:
116 | [A comprehensive study of Convergent and Commutative Replicated Data Types][CRDT].
117 | statebox was developed without knowledge of the paper, so the terminology and
118 | implementation details differ.
119 |
120 | I think the technique used by statebox would be best described as a
121 | state-based object, although the merge algorithm and event queue
122 | is similar to how op-based objects are described.
123 |
124 | [CRDT]: http://hal.archives-ouvertes.fr/inria-00555588/
--------------------------------------------------------------------------------
/test/statebox_counter_tests.erl:
--------------------------------------------------------------------------------
1 | -module(statebox_counter_tests).
2 | -behaviour(proper_statem).
3 | -export([initial_state/0, command/1,
4 | precondition/2, postcondition/3, next_state/3]).
5 | -export([apply_f_inc_acc/4, add_sibling/0, merge_siblings/1]).
6 |
7 | -include_lib("proper/include/proper.hrl").
8 | -include_lib("eunit/include/eunit.hrl").
9 |
10 | clock_step() ->
11 | 1000.
12 |
13 | default_age() ->
14 | 10 * clock_step().
15 |
16 | apply_f_inc_acc(Value, Clock, N, Counters) ->
17 | Key = {Clock, statebox_identity:entropy()},
18 | statebox:apply_op(
19 | statebox_counter:f_inc_acc(Value, default_age(), Key),
20 | lists:nth(N, Counters)).
21 |
22 | add_sibling() ->
23 | ok.
24 |
25 | merge_siblings(_N) ->
26 | ok.
27 |
28 | %% statem
29 |
30 | %% TODO:
31 | %% Generate a new sibling (add_sibling)
32 | %% Update existing counter (apply_f_inc_acc)
33 | %% Merge (up to) N siblings (merge_siblings)
34 | %% Expiration used will be for 20 clock cycles
35 | -record(state, {counters=[[]], num_counters=1, value=[], clock=0}).
36 |
37 | initial_state() ->
38 | #state{clock=10000}.
39 |
40 | command(#state{counters=Counters, num_counters=N, clock=Clock}) ->
41 | oneof([{call, ?MODULE, add_sibling, []},
42 | {call, ?MODULE, merge_siblings, [range(1, N)]},
43 | {call, ?MODULE, apply_f_inc_acc, [range(-3, 3), Clock, range(1, N), Counters]}]).
44 |
45 | precondition(_S, _Call) ->
46 | true.
47 |
48 | postcondition(S, {call, _, apply_f_inc_acc, [Inc, _]}, Res) ->
49 | sane_counter(Res)
50 | andalso (Inc + lists:sum(S#state.value)) =:= statebox_counter:value(Res);
51 | postcondition(S, {call, _, _, _}, _Res) ->
52 | lists:all(fun sane_counter/1, S#state.counters)
53 | andalso (lists:sum(S#state.value) =:=
54 | statebox_counter:value(statebox_counter:merge(S#state.counters))).
55 |
56 | next_state(S=#state{counters=[H|T]}, _V, {call, _, add_sibling, []}) ->
57 | S#state{counters=[H,H|T]};
58 | next_state(S=#state{counters=Counters}, _V, {call, _, merge_siblings, [N]}) ->
59 | {L, T} = lists:split(N, Counters),
60 | S#state{counters=[statebox_counter:merge(L) | T]};
61 | next_state(S=#state{counters=Counters, clock=Clock}, V, {call, _, apply_f_inc_acc, [Inc, Clock, N, _C]}) ->
62 | Counters1 = lists:sublist(Counters, N - 1) ++ [V | lists:nthtail(N, Counters)],
63 | S#state{counters=Counters1,
64 | value=[Inc | S#state.value],
65 | clock=Clock + clock_step()}.
66 |
67 | sane_counter([]) ->
68 | true;
69 | sane_counter([{Timestamp, Id} | Rest]) ->
70 | sane_counter(Rest, Id =:= acc, Timestamp).
71 |
72 | sane_counter([A, A | _], _, _) ->
73 | false;
74 | sane_counter([{T1, _} | _], true, T0) when T1 < T0 ->
75 | false;
76 | sane_counter([{_, acc} | _], true, _) ->
77 | false;
78 | sane_counter([{T1, acc} | Rest], false, _T0) ->
79 | sane_counter(Rest, true, T1);
80 | sane_counter([{T1, _} | Rest], HasAcc, _T0) ->
81 | sane_counter(Rest, HasAcc, T1);
82 | sane_counter([], _, _) ->
83 | true.
84 |
85 | %% properties
86 |
87 | prop_counter_works_fine() ->
88 | ?FORALL(Cmds, commands(?MODULE),
89 | ?TRAPEXIT(
90 | begin
91 | {History,State,Result} = run_commands(?MODULE, Cmds),
92 | ?WHENFAIL(io:format("History: ~w\nState: ~w\nResult: ~w\n",
93 | [History,State,Result]),
94 | aggregate(command_names(Cmds), Result =:= ok))
95 | end)).
96 |
97 | %% tests
98 |
99 | initial_test() ->
100 | ?assertEqual(
101 | 0,
102 | statebox_counter:value([])),
103 | ok.
104 |
105 | old_counter_test() ->
106 | %% Entropy part of the tuple is 0 here, we don't need it for this test.
107 | Fold = fun (T, Acc) ->
108 | statebox:apply_op(
109 | statebox_counter:f_inc_acc(1, 10, {T, 0}),
110 | Acc)
111 | end,
112 | Ctr = lists:foldl(Fold, [], lists:seq(10, 30, 2)),
113 | ?assertEqual(
114 | [{{20, acc}, 6},
115 | {{22, 0}, 1},
116 | {{24, 0}, 1},
117 | {{26, 0}, 1},
118 | {{28, 0}, 1},
119 | {{30, 0}, 1}],
120 | Ctr),
121 | ?assertEqual(
122 | 11,
123 | statebox_counter:value(Ctr)),
124 | %% Should fill in only lists:seq(21, 29, 2).
125 | Ctr1 = lists:foldl(Fold, Ctr, lists:seq(1, 30)),
126 | ?assertEqual(
127 | [{{20, acc}, 6},
128 | {{21, 0}, 1},
129 | {{22, 0}, 1},
130 | {{23, 0}, 1},
131 | {{24, 0}, 1},
132 | {{25, 0}, 1},
133 | {{26, 0}, 1},
134 | {{27, 0}, 1},
135 | {{28, 0}, 1},
136 | {{29, 0}, 1},
137 | {{30, 0}, 1}],
138 | Ctr1),
139 | ?assertEqual(
140 | 16,
141 | statebox_counter:value(Ctr1)),
142 | ok.
143 |
144 | f_inc_acc_test() ->
145 | %% We should expect to get unique enough results from our entropy and
146 | %% timestamp even if the frequency is high.
147 | ?assertEqual(
148 | 1000,
149 | statebox_counter:value(
150 | lists:foldl(
151 | fun statebox:apply_op/2,
152 | [],
153 | [statebox_counter:f_inc_acc(1, 1000) || _ <- lists:seq(1, 1000)]))),
154 | ok.
155 |
156 | inc_test() ->
157 | C0 = [],
158 | C1 = statebox_counter:inc({1, 1}, 1, C0),
159 | C2 = statebox_counter:inc({2, 2}, 1, C1),
160 | ?assertEqual(
161 | 0,
162 | statebox_counter:value(C0)),
163 | ?assertEqual(
164 | 1,
165 | statebox_counter:value(C1)),
166 | ?assertEqual(
167 | 2,
168 | statebox_counter:value(C2)),
169 | ok.
170 |
171 | merge_test() ->
172 | C0 = [],
173 | C1 = statebox_counter:inc({1, 1}, 1, C0),
174 | C2 = statebox_counter:inc({2, 2}, 1, C1),
175 | ?assertEqual(
176 | 2,
177 | statebox_counter:value(statebox_counter:merge([C0, C1, C2]))),
178 | ?assertEqual(
179 | 1,
180 | statebox_counter:value(statebox_counter:merge([C0, C1, C1]))),
181 | ?assertEqual(
182 | 1,
183 | statebox_counter:value(statebox_counter:merge([C1]))),
184 | ?assertEqual(
185 | 0,
186 | statebox_counter:value(statebox_counter:merge([C0, C0]))),
187 | ok.
188 |
189 | next_clock(N) ->
190 | Next = N + clock_step(),
191 | meck:expect(statebox_clock, timestamp, fun () -> next_clock(Next) end),
192 | Next.
193 |
194 | setup_clock() ->
195 | meck:new(statebox_clock, [passthrough]),
196 | next_clock(100000).
197 |
198 | cleanup_clock(_) ->
199 | meck:unload(statebox_clock).
200 |
201 | proper_test_() ->
202 | {foreach,
203 | fun setup_clock/0,
204 | fun cleanup_clock/1,
205 | [{atom_to_list(F),
206 | fun () -> ?assert(proper:quickcheck(?MODULE:F(), [long_result])) end}
207 | || {F, 0} <- ?MODULE:module_info(exports), F > 'prop_', F < 'prop`']}.
208 |
--------------------------------------------------------------------------------
/src/statebox_orddict.erl:
--------------------------------------------------------------------------------
1 | %% @doc Statebox wrappers for orddict-based storage.
2 | -module(statebox_orddict).
3 | -export([from_values/1, orddict_from_proplist/1]).
4 | -export([is_empty/1]).
5 | -export([f_union/2, f_subtract/2, f_update/2, f_merge/1,
6 | f_merge_proplist/1, f_store/2, f_delete/1, f_erase/1]).
7 | -export([op_union/3, op_subtract/3, op_update/3, op_merge/2]).
8 |
9 | %% External API
10 |
11 | -type proplist() :: [{term(), term()}].
12 | -type orddict() :: proplist().
13 | -type statebox() :: statebox:statebox().
14 | -type op() :: statebox:op().
15 |
16 | %% @doc Return a statebox() from a list of statebox() and/or proplist().
17 | %% proplist() are convered to a new statebox() with f_merge_proplist/2
18 | %% before merge.
19 | -spec from_values([proplist() | statebox()]) -> statebox().
20 | from_values([]) ->
21 | statebox:new(fun () -> [] end);
22 | from_values(Vals) ->
23 | statebox:merge([as_statebox(V) || V <- Vals]).
24 |
25 | %% @doc Convert a proplist() to an orddict().
26 | %% Only [{term(), term()}] proplists are supported.
27 | -spec orddict_from_proplist(proplist()) -> orddict().
28 | orddict_from_proplist(P) ->
29 | orddict:from_list(P).
30 |
31 | %% @doc Return true if the statebox's value is [], false otherwise.
32 | -spec is_empty(statebox()) -> boolean().
33 | is_empty(Box) ->
34 | statebox:value(Box) =:= [].
35 |
36 | %% @doc Returns an op() that does an ordsets:union(New, Set) on the value at
37 | %% K in orddict (or [] if not present).
38 | -spec f_union(term(), [term()]) -> op().
39 | f_union(K, New) ->
40 | {fun ?MODULE:op_union/3, [K, New]}.
41 |
42 | %% @doc Returns an op() that does an ordsets:subtract(Set, Del) on the value at
43 | %% K in orddict (or [] if not present).
44 | -spec f_subtract(term(), [term()]) -> op().
45 | f_subtract(K, Del) ->
46 | {fun ?MODULE:op_subtract/3, [K, Del]}.
47 |
48 | %% @doc Returns an op() that merges the proplist New to the orddict.
49 | -spec f_merge_proplist(term()) -> op().
50 | f_merge_proplist(New) ->
51 | f_merge(orddict_from_proplist(New)).
52 |
53 | %% @doc Returns an op() that merges the orddict New to the orddict.
54 | -spec f_merge(term()) -> op().
55 | f_merge(New) ->
56 | {fun ?MODULE:op_merge/2, [New]}.
57 |
58 | %% @doc Returns an op() that updates the value at Key in orddict (or [] if
59 | %% not present) with the given Op.
60 | -spec f_update(term(), op()) -> op().
61 | f_update(Key, Op) ->
62 | {fun ?MODULE:op_update/3, [Key, Op]}.
63 |
64 | %% @doc Returns an op() that stores Value at Key in orddict.
65 | -spec f_store(term(), term()) -> op().
66 | f_store(Key, Value) ->
67 | {fun orddict:store/3, [Key, Value]}.
68 |
69 | %% @doc Returns an op() that deletes the pair if present from orddict.
70 | -spec f_delete({term(), term()}) -> op().
71 | f_delete(Pair={_, _}) ->
72 | {fun lists:delete/2, [Pair]}.
73 |
74 | %% @doc Returns an op() that erases the value at K in orddict.
75 | -spec f_erase(term()) -> op().
76 | f_erase(Key) ->
77 | {fun orddict:erase/2, [Key]}.
78 |
79 | %% Statebox ops
80 |
81 | %% @private
82 | op_union(K, New, D) ->
83 | orddict:update(K, fun (Old) -> ordsets:union(Old, New) end, New, D).
84 |
85 | %% @private
86 | op_subtract(K, Del, D) ->
87 | orddict:update(K, fun (Old) -> ordsets:subtract(Old, Del) end, [], D).
88 |
89 | %% @private
90 | op_merge(New, D) ->
91 | orddict:merge(fun (_Key, _OldV, NewV) -> NewV end, D, New).
92 |
93 | %% @private
94 | op_update(Key, Op, [{K, _}=E | Dict]) when Key < K ->
95 | %% This is very similar to orddict:update/4.
96 | [{Key, statebox:apply_op(Op, [])}, E | Dict];
97 | op_update(Key, Op, [{K, _}=E | Dict]) when Key > K ->
98 | [E | op_update(Key, Op, Dict)];
99 | op_update(Key, Op, [{_K, Val} | Dict]) -> %% Key == K
100 | [{Key, statebox:apply_op(Op, Val)} | Dict];
101 | op_update(Key, Op, []) ->
102 | [{Key, statebox:apply_op(Op, [])}].
103 |
104 | %% Internal API
105 |
106 | as_statebox(V) ->
107 | case statebox:is_statebox(V) of
108 | true ->
109 | V;
110 | false ->
111 | from_legacy(V)
112 | end.
113 |
114 | from_legacy(D) ->
115 | %% Legacy objects should always be overwritten.
116 | statebox:modify(f_merge_proplist(D), statebox:new(fun () -> [] end)).
117 |
118 | -ifdef(TEST).
119 | -include_lib("eunit/include/eunit.hrl").
120 | orddict_from_proplist_test() ->
121 | ?assertEqual(
122 | [{a, b}, {c, d}],
123 | orddict_from_proplist([{c, d}, {a, b}])),
124 | ?assertEqual(
125 | [{a, c}],
126 | orddict_from_proplist([{a, b}, {a, c}])),
127 | ?assertEqual(
128 | [{a, b}],
129 | orddict_from_proplist([{a, c}, {a, b}])),
130 | ok.
131 |
132 | is_empty_test() ->
133 | ?assertEqual(
134 | true,
135 | is_empty(statebox:new(fun () -> [] end))),
136 | ?assertEqual(
137 | false,
138 | is_empty(
139 | statebox:modify(f_store(foo, bar),
140 | statebox:new(fun () -> [] end)))),
141 | ok.
142 |
143 | f_subtract_test() ->
144 | ?assertEqual(
145 | [{taco, [a, b]}],
146 | statebox:apply_op(f_subtract(taco, [c, d]), [{taco, [a, b, c]}])),
147 | ok.
148 |
149 | f_merge_proplist_test() ->
150 | ?assertEqual(
151 | [{a, b}, {c, d}, {e, f}],
152 | statebox:apply_op(f_merge_proplist([{e, f}, {c, d}]), [{a, b}])),
153 | ok.
154 |
155 | f_merge_test() ->
156 | ?assertEqual(
157 | [{a, b}, {c, d}, {e, f}],
158 | statebox:apply_op(f_merge([{a, b}, {c, d}, {e, f}]), [{a, a}])),
159 | ok.
160 |
161 | f_update_test() ->
162 | ?assertEqual(
163 | [{b, [{b, c}]}],
164 | statebox:apply_op(f_update(b, f_store(b, c)), [])),
165 | ?assertEqual(
166 | [{b, [{b, c}]}, {c, c}],
167 | statebox:apply_op(f_update(b, f_store(b, c)), [{c, c}])),
168 | ?assertEqual(
169 | [{a, a}, {b, [{a, a}, {b, c}]}],
170 | statebox:apply_op(f_update(b, f_store(b, c)), [{a, a}, {b, [{a, a}]}])),
171 | ok.
172 |
173 | f_store_test() ->
174 | ?assertEqual(
175 | [{a, b}],
176 | statebox:apply_op(f_store(a, b), [])),
177 | ok.
178 |
179 | f_delete_test() ->
180 | ?assertEqual(
181 | [{a, b}],
182 | statebox:apply_op(f_delete({a, a}), [{a, b}])),
183 | ?assertEqual(
184 | [],
185 | statebox:apply_op(f_delete({a, b}), [{a, b}])),
186 | ok.
187 |
188 | f_erase_test() ->
189 | ?assertEqual(
190 | [{a, b}],
191 | statebox:apply_op(f_erase(b), [{a, b}])),
192 | ?assertEqual(
193 | [],
194 | statebox:apply_op(f_erase(a), [{a, b}])),
195 | ok.
196 |
197 | f_union_test() ->
198 | ?assertEqual(
199 | [{taco, [a, b, c]}],
200 | statebox:apply_op(f_union(taco, [a, b]), [{taco, [c]}])),
201 | ?assertEqual(
202 | [{taco, [a, b]}],
203 | statebox:apply_op(f_union(taco, [a, b]), [])),
204 | ok.
205 |
206 | from_values_test() ->
207 | ?assertEqual(
208 | [],
209 | statebox:value(from_values([]))),
210 | ?assertEqual(
211 | [],
212 | statebox:value(from_values([[]]))),
213 | ?assertEqual(
214 | [{<<"binary">>, value}],
215 | statebox:value(from_values([[{<<"binary">>, value}]]))),
216 | ?assertEqual(
217 | [{atom, value}],
218 | statebox:value(from_values([[{atom, value}]]))),
219 | ?assertEqual(
220 | [{services, [{key, [1, 2]}]}, {x, y}],
221 | statebox:value(from_values([[{x, y}, {services, [{key, [1, 2]}]}]]))),
222 | ?assertEqual(
223 | [],
224 | statebox:value(from_values([statebox:new(fun () -> [] end)]))),
225 | ?assertEqual(
226 | [{key, value}],
227 | statebox:value(from_values([from_values([[{key, value}]])]))),
228 | ok.
229 |
230 | readme_orddict_test() ->
231 | New = statebox_orddict:from_values([]),
232 | ChildA = statebox:modify([statebox_orddict:f_store(a, 1),
233 | statebox_orddict:f_union(c, [a, aa])],
234 | New),
235 | ChildB = statebox:modify([statebox_orddict:f_store(b, 1),
236 | statebox_orddict:f_union(c, [b, bb])],
237 | New),
238 | Resolved = statebox_orddict:from_values([ChildA, ChildB]),
239 | ?assertEqual(
240 | [{a, 1}, {b, 1}, {c, [a, aa, b, bb]}],
241 | statebox:value(Resolved)),
242 | ok.
243 |
244 | -endif.
245 |
--------------------------------------------------------------------------------
/src/statebox.erl:
--------------------------------------------------------------------------------
1 | %% @doc A "monad" for wrapping a value with a ordered event queue
2 | %% such that values that have diverged in history can be merged
3 | %% automatically in a predictable manner.
4 | %%
5 | %% In order to provide for an efficient serialization, old events
6 | %% can be expired with expire/2 and the event queue can be
7 | %% truncated to a specific maximum size with truncate/2.
8 | %%
9 | %% The default representation for a timestamp is OS clock msecs,
10 | %% defined by statebox_clock:timestamp/0. This is
11 | %% used by the convenience functions new/1 and
12 | %% modify/2.
13 | -module(statebox).
14 | -export([new/2, modify/3, merge/1, expire/2, truncate/2,
15 | new/1, modify/2,
16 | value/1, last_modified/1, is_statebox/1, apply_op/2]).
17 |
18 | -record(statebox, {
19 | value :: term(),
20 | %% sorted list of operations (oldest first).
21 | queue :: [event()],
22 | last_modified :: timestamp()}).
23 | -opaque statebox() :: #statebox{}.
24 | -type event() :: {timestamp(), op()}.
25 | -type timestamp() :: statebox_clock:timestamp().
26 | -type timedelta() :: integer().
27 | -type basic_op() :: {module(), atom(), [term()]} |
28 | {fun((term(), term()) -> statebox()) |
29 | fun((term(), term(), term()) -> statebox()), [term()]}.
30 | -type op() :: basic_op() | [op()].
31 | -export_type([statebox/0, event/0, timestamp/0, timedelta/0, basic_op/0, op/0]).
32 |
33 | %% Used in a test, must be done before function definitions.
34 | -ifdef(TEST).
35 | -export([dummy_mfa_4/4]).
36 | -endif.
37 |
38 | %% @doc Return true if the argument is a statebox, false
39 | %% otherwise.
40 | -spec is_statebox(statebox() | term()) -> boolean().
41 | is_statebox(#statebox{}) ->
42 | true;
43 | is_statebox(_T) ->
44 | false.
45 |
46 | %% @doc Construct a statebox at statebox_clock:timestamp()
47 | %% containing the result of Constructor(). This should
48 | %% return an "empty" object of the desired type, such as
49 | %% fun gb_trees:empty/0.
50 | %% @equiv new(timestamp(), Constructor)
51 | -spec new(fun(() -> term())) -> statebox().
52 | new(Constructor) ->
53 | new(statebox_clock:timestamp(), Constructor).
54 |
55 | %% @doc Construct a statebox containing the result of
56 | %% Constructor(). This should return an "empty" object of
57 | %% the desired type, such as fun gb_trees:empty/0.
58 | -spec new(timestamp(), fun(() -> term())) -> statebox().
59 | new(T, Constructor) ->
60 | new(T, Constructor(), []).
61 |
62 | %% @doc Return the current value of the statebox. You should consider this
63 | %% value to be read-only.
64 | -spec value(statebox()) -> term().
65 | value(#statebox{value=V}) ->
66 | V.
67 |
68 | %% @doc Return the last modified timestamp of the statebox.
69 | -spec last_modified(statebox()) -> timestamp().
70 | last_modified(#statebox{last_modified=T}) ->
71 | T.
72 |
73 | %% @doc Remove all events older than last_modified(S) - Age
74 | %% from the event queue.
75 | -spec expire(timedelta(), statebox()) -> statebox().
76 | expire(Age, State=#statebox{queue=Q, last_modified=T}) ->
77 | OldT = T - Age,
78 | State#statebox{
79 | queue=lists:dropwhile(fun ({EventT, _}) -> EventT < OldT end, Q)}.
80 |
81 | %% @doc Truncate the event queue to the newest N events.
82 | -spec truncate(non_neg_integer(), statebox()) -> statebox().
83 | truncate(N, State=#statebox{queue=Q}) ->
84 | case length(Q) - N of
85 | Tail when Tail > 0 ->
86 | State#statebox{queue=lists:nthtail(Tail, Q)};
87 | _ ->
88 | State
89 | end.
90 |
91 | %% @doc Return a new statebox as the product of all in-order events applied to
92 | %% the last modified statebox(). If two events occur at the same time, the
93 | %% event that sorts lowest by value will be applied first.
94 | -spec merge([statebox()]) -> statebox().
95 | merge([State]) ->
96 | State;
97 | merge(Unordered) ->
98 | #statebox{value=V, last_modified=T} = newest(Unordered),
99 | Queue = lists:umerge([Q || #statebox{queue=Q} <- Unordered]),
100 | new(T, apply_queue(V, Queue), Queue).
101 |
102 | %% @doc Modify the value in statebox and add {T, Op} to its event queue.
103 | %% Op should be a {M, F, Args} or {Fun, Args}.
104 | %% The value will be transformed as such:
105 | %% NewValue = apply(Fun, Args ++ [value(S)]).
106 | %% The operation should be repeatable and should return the same type as
107 | %% value(S). This means that this should hold true:
108 | %% Fun(Arg, S) =:= Fun(Arg, Fun(Arg, S)).
109 | %% An example of this kind of operation is orddict:store/3.
110 | %% Only exported operations should be used in order to ensure that the
111 | %% serialization is small and robust (this is not enforced).
112 | -spec modify(timestamp(), op(), statebox()) -> statebox().
113 | modify(T, Op, #statebox{value=Value, queue=Queue, last_modified=OldT})
114 | when OldT =< T ->
115 | new(T, apply_op(Op, Value), queue_in({T, Op}, Queue));
116 | modify(T, _Op, #statebox{last_modified=OldT}) ->
117 | throw({invalid_timestamp, {T, '<', OldT}}).
118 |
119 | %% @doc Modify a statebox at timestamp
120 | %% max(1 + last_modified(S), statebox_clock:timestamp()).
121 | %% See modify/3 for more information.
122 | %% @equiv modify(max(1 + last_modified(S), statebox_clock:timestamp()), Op, S)
123 | -spec modify(op(), statebox()) -> statebox().
124 | modify(Op, S) ->
125 | modify(max(1 + last_modified(S), statebox_clock:timestamp()), Op, S).
126 |
127 | %% @doc Apply an op() to Data.
128 | -spec apply_op(op(), term()) -> term().
129 | apply_op({F, [A]}, Data) when is_function(F, 2) ->
130 | F(A, Data);
131 | apply_op({F, [A, B]}, Data) when is_function(F, 3) ->
132 | F(A, B, Data);
133 | apply_op({F, [A, B, C]}, Data) when is_function(F, 4) ->
134 | F(A, B, C, Data);
135 | apply_op({F, A}, Data) when is_function(F) ->
136 | apply(F, A ++ [Data]);
137 | apply_op({M, F, [A]}, Data) ->
138 | M:F(A, Data);
139 | apply_op({M, F, [A, B]}, Data) ->
140 | M:F(A, B, Data);
141 | apply_op({M, F, A}, Data) ->
142 | apply(M, F, A ++ [Data]);
143 | apply_op([Op | Rest], Data) ->
144 | apply_op(Rest, apply_op(Op, Data));
145 | apply_op([], Data) ->
146 | Data.
147 |
148 | %% Internal API
149 |
150 | newest([First | Rest]) ->
151 | newest(First, Rest).
152 |
153 | newest(M0, [M1 | Rest]) ->
154 | case last_modified(M0) >= last_modified(M1) of
155 | true ->
156 | newest(M0, Rest);
157 | false ->
158 | newest(M1, Rest)
159 | end;
160 | newest(M, []) ->
161 | M.
162 |
163 | new(T, V, Q) ->
164 | #statebox{value=V, queue=Q, last_modified=T}.
165 |
166 | queue_in(Event, Queue) ->
167 | Queue ++ [Event].
168 |
169 | apply_queue(Data, [{_T, Op} | Rest]) ->
170 | apply_queue(apply_op(Op, Data), Rest);
171 | apply_queue(Data, []) ->
172 | Data.
173 |
174 | -ifdef(TEST).
175 | -include_lib("eunit/include/eunit.hrl").
176 | new_test() ->
177 | Now = 1,
178 | S = new(Now, fun () -> data end),
179 | ?assertEqual(
180 | data,
181 | value(S)),
182 | ?assertEqual(
183 | Now,
184 | last_modified(S)),
185 | %% Nothing to expire
186 | ?assertEqual(
187 | S,
188 | expire(0, S)),
189 | %% Nothing to truncate
190 | ?assertEqual(
191 | S,
192 | truncate(16, S)),
193 | %% Nothing to merge
194 | ?assertEqual(
195 | S,
196 | merge([S])),
197 | %% Merging the same object
198 | ?assertEqual(
199 | S,
200 | merge([S, S])),
201 | ok.
202 |
203 | bad_modify_test() ->
204 | F = fun (N, S) -> modify(N, {fun ordsets:add_element/2, [N]}, S) end,
205 | S10 = lists:foldl(F, new(0, fun () -> [] end), lists:seq(1, 10)),
206 | ?assertEqual(
207 | lists:seq(1, 10),
208 | value(S10)),
209 | ?assertThrow(
210 | {invalid_timestamp, {9, '<', 10}},
211 | F(9, S10)),
212 | ok.
213 |
214 | %% @private
215 | dummy_mfa_4(a, b, C, D) ->
216 | ordsets:add_element(C, D).
217 |
218 | batch_apply_op_test() ->
219 | S = new(0, fun () -> [] end),
220 | S0 = modify([], S),
221 | S1 = modify([{ordsets, add_element, [N]} || N <- lists:seq(1, 1)], S),
222 | S10 = modify([{ordsets, add_element, [N]} || N <- lists:seq(1, 10)], S),
223 | ?assertEqual(
224 | [],
225 | value(S0)),
226 | ?assertEqual(
227 | lists:seq(1, 1),
228 | value(S1)),
229 | ?assertEqual(
230 | lists:seq(1, 10),
231 | value(S10)),
232 | ok.
233 |
234 | apply_op_5_test() ->
235 | ?assertEqual(
236 | [a, b, c, d, e],
237 | statebox:apply_op(
238 | {fun (A, B, C, D, E) -> [A, B, C, D, E] end, [a, b, c, d]},
239 | e)).
240 |
241 | alt_apply_op_test() ->
242 | L = [fun (N=1) -> {ordsets, add_element, [N]} end,
243 | fun (N=2) ->
244 | {fun (a, B, C) -> ordsets:add_element(B, C) end, [a, N]}
245 | end,
246 | fun (N=3) ->
247 | {fun ?MODULE:dummy_mfa_4/4, [a, b, N]}
248 | end,
249 | fun (N=4) ->
250 | {?MODULE, dummy_mfa_4, [a, b, N]}
251 | end,
252 | fun (N=5) ->
253 | {ordsets, fold,
254 | [fun (X, Acc) -> ordsets:add_element(X + N, Acc) end, []]}
255 | end],
256 | F = fun ({N, F}, S) -> modify(N, F(N), S) end,
257 | S5 = lists:foldl(F, new(0, fun () -> [] end),
258 | lists:zip(lists:seq(1, 5), L)),
259 | ?assertEqual(
260 | lists:seq(5 + 1, 5 + 4),
261 | value(S5)),
262 | ok.
263 |
264 | truncate_test() ->
265 | F = fun (N, S) -> modify(N, {fun ordsets:add_element/2, [N]}, S) end,
266 | S10 = lists:foldl(F, new(0, fun () -> [] end), lists:seq(1, 10)),
267 | ?assertEqual(
268 | lists:seq(1, 10),
269 | value(S10)),
270 | ?assertEqual(
271 | 10,
272 | length(S10#statebox.queue)),
273 | ?assertEqual(
274 | 10,
275 | length((truncate(20, S10))#statebox.queue)),
276 | ?assertEqual(
277 | 10,
278 | length((truncate(10, S10))#statebox.queue)),
279 | ?assertEqual(
280 | 1,
281 | length((truncate(1, S10))#statebox.queue)),
282 | ok.
283 |
284 | expire_test() ->
285 | F = fun (N, S) -> modify(N, {fun ordsets:add_element/2, [N]}, S) end,
286 | S10 = lists:foldl(F, new(0, fun () -> [] end), lists:seq(1, 10)),
287 | ?assertEqual(
288 | lists:seq(1, 10),
289 | value(S10)),
290 | ?assertEqual(
291 | 10,
292 | length(S10#statebox.queue)),
293 | ?assertEqual(
294 | 1,
295 | length((expire(0, S10))#statebox.queue)),
296 | ?assertEqual(
297 | 10,
298 | length((expire(10, S10))#statebox.queue)),
299 | ?assertEqual(
300 | 10,
301 | length((expire(11, S10))#statebox.queue)),
302 | ok.
303 |
304 | orddict_in_a_statebox_test() ->
305 | S0 = new(0, fun () -> [] end),
306 | ?assertEqual(
307 | [],
308 | value(S0)),
309 | S1_a = modify(1, {fun orddict:store/3, [key, a]}, S0),
310 | S1_b = modify(1, {fun orddict:store/3, [key, b]}, S0),
311 | S1_c = modify(1, {fun orddict:store/3, [c, c]}, S0),
312 | S2_aa = modify(3, {fun orddict:store/3, [key, a2]}, S1_a),
313 | S2_ab = modify(2, {fun orddict:store/3, [key, b2]}, S1_a),
314 | S2_bb = modify(2, {fun orddict:store/3, [key, b2]}, S1_b),
315 | ?assertEqual(
316 | 1,
317 | last_modified(S1_a)),
318 | ?assertEqual(
319 | 1,
320 | last_modified(S1_b)),
321 | ?assertEqual(
322 | [{key, a}],
323 | value(S1_a)),
324 | ?assertEqual(
325 | [{key, b}],
326 | value(S1_b)),
327 | ?assertEqual(
328 | S1_a,
329 | merge([S1_a])),
330 | ?assertEqual(
331 | S1_a,
332 | merge([S0, S1_a])),
333 | ?assertEqual(
334 | S1_a,
335 | merge([S1_a, S0])),
336 | %% This is a conflict that can not be resolved peacefully,
337 | %% but S1_b wins by op compare
338 | ?assertEqual(
339 | value(S1_b),
340 | value(merge([S1_a, S1_b]))),
341 | %% This is a conflict that can not be resolved peacefully,
342 | %% but S1_b wins by op compare
343 | ?assertEqual(
344 | value(S1_b),
345 | value(merge([S1_b, S1_a]))),
346 | %% S2_aa wins because it has a bigger timestamp
347 | ?assertEqual(
348 | value(S2_aa),
349 | value(merge([S2_aa, S2_ab]))),
350 | %% S2_aa wins because it has a bigger timestamp
351 | ?assertEqual(
352 | value(S2_aa),
353 | value(merge([S2_ab, S2_aa]))),
354 | %% S2_aa wins because it has a bigger timestamp
355 | ?assertEqual(
356 | value(S2_aa),
357 | value(merge([S2_bb, S2_aa]))),
358 | %% S2_aa wins because it has a bigger timestamp
359 | ?assertEqual(
360 | value(S2_aa),
361 | value(merge([S2_aa, S2_bb]))),
362 | %% S1_[ab] and S1_c collide in time but the operations do not conflict
363 | ?assertEqual(
364 | [{c, c}, {key, a}],
365 | value(merge([S1_a, S1_c]))),
366 | ?assertEqual(
367 | [{c, c}, {key, a}],
368 | value(merge([S1_c, S1_a]))),
369 | ?assertEqual(
370 | [{c, c}, {key, b}],
371 | value(merge([S1_b, S1_c]))),
372 | ?assertEqual(
373 | [{c, c}, {key, b}],
374 | value(merge([S1_c, S1_b]))),
375 | %% S1_b wins over S1_a by op compare but S1_c is independent
376 | ?assertEqual(
377 | [{c, c}, {key, b}],
378 | value(merge([S1_c, S1_a, S1_b]))),
379 | ?assertEqual(
380 | [{c, c}, {key, b}],
381 | value(merge([S1_c, S1_b, S1_a]))),
382 | ?assertEqual(
383 | [{c, c}, {key, b}],
384 | value(merge([S1_a, S1_b, S1_c]))),
385 | ?assertEqual(
386 | [{c, c}, {key, b}],
387 | value(merge([S1_a, S1_c, S1_b]))),
388 | ok.
389 |
390 | -define(WHENEVER, 1303513575954).
391 | convenience_test_() ->
392 | {setup,
393 | fun () ->
394 | meck:new(statebox_clock),
395 | meck:expect(statebox_clock, timestamp, 0, ?WHENEVER)
396 | end,
397 | fun (_) -> meck:unload(statebox_clock) end,
398 | [{"new",
399 | fun () ->
400 | ?assertEqual(
401 | ?WHENEVER,
402 | last_modified(new(fun () -> [] end)))
403 | end},
404 | {"modify",
405 | fun () ->
406 | S = modify({fun ordsets:add_element/2, [a]},
407 | new(0, fun () -> [] end)),
408 | S1 = modify({fun ordsets:add_element/2, [b]},
409 | S),
410 | ?assertEqual(
411 | ?WHENEVER,
412 | last_modified(S)),
413 | ?assertEqual(
414 | [a],
415 | value(S)),
416 | %% Check for clock skew correction
417 | ?assertEqual(
418 | 1 + ?WHENEVER,
419 | last_modified(S1)),
420 | ?assertEqual(
421 | [a, b],
422 | value(S1))
423 | end}]}.
424 |
425 | readme_ordsets_test() ->
426 | New = statebox:new(fun () -> [] end),
427 | ChildA = statebox:modify({fun ordsets:add_element/2, [a]}, New),
428 | ChildB = statebox:modify({fun ordsets:add_element/2, [b]}, New),
429 | Resolved = statebox:merge([ChildA, ChildB]),
430 | ?assertEqual(
431 | [a, b],
432 | statebox:value(Resolved)).
433 |
434 | readme_ordsets_manual_test() ->
435 | New = statebox:new(0, fun () -> [] end),
436 | ChildA = statebox:modify(1, {fun ordsets:add_element/2, [a]}, New),
437 | ChildB = statebox:modify(2, {fun ordsets:add_element/2, [b]}, New),
438 | Resolved = statebox:merge([ChildA, ChildB]),
439 | ?assertEqual(
440 | [a, b],
441 | statebox:value(Resolved)).
442 |
443 | is_statebox_test() ->
444 | ?assertEqual(
445 | false,
446 | is_statebox(not_a_statebox)),
447 | ?assertEqual(
448 | true,
449 | is_statebox(new(fun () -> is_a_statebox end))),
450 | ok.
451 |
452 | -endif.
453 |
--------------------------------------------------------------------------------