├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── rebar ├── rebar.config ├── src ├── statebox.app.src ├── statebox.erl ├── statebox_clock.erl ├── statebox_counter.erl ├── statebox_identity.erl └── statebox_orddict.erl └── test ├── statebox_counter_tests.erl └── statebox_identity_tests.erl /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mochi/statebox/680bccb0a25a2658b70e14a4d21dcefaf663fd6a/rebar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------