From b713ce60a8ded29fcd6a3c957da8477bb256e177 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Mon, 21 Jan 2019 10:51:07 +0000 Subject: [PATCH 01/15] Initial eqc setup --- .eqc-info | Bin 0 -> 8945 bytes current_counterexample.eqc | Bin 0 -> 1034 bytes rebar.config | 4 +- src/leveled_inker.erl | 2 +- test/leveledjc_eqc.erl | 1065 ++++++++++++++++++++++++++++++++++++ 5 files changed, 1068 insertions(+), 3 deletions(-) create mode 100644 .eqc-info create mode 100644 current_counterexample.eqc create mode 100644 test/leveledjc_eqc.erl diff --git a/.eqc-info b/.eqc-info new file mode 100644 index 0000000000000000000000000000000000000000..7dba85ac8475d61c59a9871e02448ff8a3ad8622 GIT binary patch literal 8945 zcmeI2U2GIp6vxke?QUDTE|s>T<)b7LY79c5NX3V~s81*wN=OW@cV}*QW|^Hk%+A(Q zNe~I~$*55@Au&YM7aLyuNI?*tlbiqFsH^RxL=6Ajho+*j8<=0Pw_F zdgz~_hwV?Ij$1nc()xT5P`=do-_4a1eEzd0sd^y}@E9ulE_SI&mBoct!F!m{nJ~)4 z!mb7FE*`}$wn}Cm?=fwSHw$_5Q4}D>)-BLYBpu`MU!%~9>YJvy4jS;uZhoo_b%hZu{p-P?d5r)*ABv=9518`pC$u#8JXdAE&Hq}2d+~IM~KS{2G4*ZmD5bfowik< zy8XC1vijV|+fSc+x@+{$_m53H(?c(Ivx&H}R-*2LwtmywMS9&M9<8QB9O_MUn0i{Z^b!5h=Bp8Dq`Tk8`8?5A$$AcLRb zQ*64lg3}VtlD##&~q4Z@O zFaGt?#pA}T^7X?Vu4UMy8 zqdE&P5Y=?R!%>|BI2_fBLE#@}b$cD4#`K+_$Mh1&#B?X*V!8`f#&kCf#q?e9LQF4J n#D`(s{@rR^-=oKMPbRLH<>Gqz%D7%JB=jP|f88L!stf-D^C8Rz literal 0 HcmV?d00001 diff --git a/current_counterexample.eqc b/current_counterexample.eqc new file mode 100644 index 0000000000000000000000000000000000000000..775fd177f75722d4ee2fa79f960f1545d15c6e25 GIT binary patch literal 1034 zcmbW0&u>jZ6vyZOnD?$m(T#3|*htwSMQdXtRzw7m2-mzj^X`w%yK_5t?xQwB(^!&V z(?lA@s_8~-BvkwdqSz?@0||n}s%K{UTJho~dXu?1bMBcl-}5==PI82ho46EtB^%e9 zn%>}+po~-0&sm#sMzbhn<5A+7FlEBqL=q?QDe@!2yFw}2nC7Vp8zdTIIn{~bFczU4 zYnzSEu|W!k@B!`pk{ehmIxGt#A&$M%H*nxLCsm}(wJREW8AhdAJHgkX(S0X2#@uwvr5%INM zYcQ!&Mhu(I#tw@;QU4=Tkij+j!kx~&a}TZ^efQ<#bl<6;OMB+=$O|$wJJtVn`<;bP z3nzm&5VRTA>fz_?kaUA8> %% @doc %% Shutdown any snapshots before closing the store shutdown_snapshots(Snapshots) -> - lists:foreach(fun({Snap, _SQN}) -> ok = ink_close(Snap) end, Snapshots). + lists:foreach(fun({Snap, _TS, _SQN}) -> ok = ink_close(Snap) end, Snapshots). -spec shutdown_manifest(leveled_imanifest:manifest()) -> ok. %% @doc diff --git a/test/leveledjc_eqc.erl b/test/leveledjc_eqc.erl new file mode 100644 index 0000000..eec9a64 --- /dev/null +++ b/test/leveledjc_eqc.erl @@ -0,0 +1,1065 @@ +%% ------------------------------------------------------------------- +%% +%% leveld_eqc: basic statem for doing things to leveled +%% +%% 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(leveledjc_eqc). + +-include_lib("eqc/include/eqc.hrl"). +-include_lib("eqc/include/eqc_statem.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include("../include/leveled.hrl"). + +-compile([export_all, nowarn_export_all]). + +-define(NUMTESTS, 1000). +-define(QC_OUT(P), + eqc:on_output(fun(Str, Args) -> + io:format(user, Str, Args) end, P)). + +-define(CMD_VALID(State, Cmd, True, False), + case is_valid_cmd(State, Cmd) of + true -> True; + false -> False + end). + + +eqc_test_() -> + Timeout = 50, + {timeout, max(2 * Timeout, Timeout + 10), + ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(Timeout, ?QC_OUT(prop_db()))))}. + +run() -> + run(?NUMTESTS). + +run(Count) -> + eqc:quickcheck(eqc:numtests(Count, prop_db())). + +check() -> + eqc:check(prop_db()). + +iff(B1, B2) -> B1 == B2. +implies(B1, B2) -> (not B1 orelse B2). + +%% start_opts should not be added to this map, it is added only when the system is started the first time. +initial_state() -> + #{dir => {var, dir}, + sut => sut, + leveled => undefined, %% to make adapt happy after failing pre/1 + counter => 0, + model => orddict:new(), + previous_keys => [], + deleted_keys => [], + folders => [] + }. + +%% --- Operation: init_backend --- +init_backend_pre(S) -> + not is_leveled_open(S). + +init_backend_args(#{dir := Dir, sut := Name} = S) -> + case maps:get(start_opts, S, undefined) of + undefined -> + [ default(?RIAK_TAG, ?STD_TAG), %% Just test one tag at a time + [{root_path, Dir}, {log_level, error} | gen_opts()], Name ]; + Opts -> + %% root_path is part of existing options + [ maps:get(tag, S), Opts, Name ] + end. + +init_backend_pre(S, [Tag, Options, _]) -> + %% for shrinking + PreviousOptions = maps:get(start_opts, S, undefined), + maps:get(tag, S, Tag) == Tag andalso + PreviousOptions == undefined orelse PreviousOptions == Options. + +init_backend_adapt(S, [Tag, Options, Name]) -> + [ maps:get(tag, S, Tag), maps:get(start_opts, S, Options), Name]. + +%% @doc init_backend - The actual operation +%% Start the database and read data from disk +init_backend(_Tag, Options, Name) -> + case leveled_bookie:book_start(Options) of + {ok, Bookie} -> + unlink(Bookie), + erlang:register(Name, Bookie), + Bookie; + Error -> Error + end. + +init_backend_next(S, LevelEdPid, [Tag, Options, _]) -> + S#{leveled => LevelEdPid, start_opts => Options, tag => Tag}. + +init_backend_post(_S, [_, _Options, _], LevelEdPid) -> + is_pid(LevelEdPid). + +init_backend_features(_S, [_Tag, Options, _], _Res) -> + [{start_options, Options}]. + + +%% --- Operation: stop --- +stop_pre(S) -> + is_leveled_open(S). + +%% @doc stop_args - Argument generator +stop_args(#{leveled := Pid}) -> + [Pid]. + +stop_pre(#{leveled := Leveled}, [Pid]) -> + %% check during shrinking + Pid == Leveled. + +stop_adapt(#{leveled := Leveled}, [_]) -> + [Leveled]. + +%% @doc stop - The actual operation +%% Stop the server, but the values are still on disk +stop(Pid) -> + ok = leveled_bookie:book_close(Pid). + +stop_next(S, _Value, [_Pid]) -> + S#{leveled => undefined, + folders => [], + used_folders => [], + stop_folders => maps:get(folders, S, []) ++ maps:get(used_folders, S, [])}. + +stop_post(_S, [Pid], _Res) -> + Mon = erlang:monitor(process, Pid), + receive + {'DOWN', Mon, _Type, Pid, _Info} -> + true + after 5000 -> + {still_a_pid, Pid} + end. + + +%% --- Operation: put --- +put_pre(S) -> + is_leveled_open(S). + +put_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> + ?LET(Categories, gen_categories(Tag), + ?LET({{Key, Bucket}, Value, IndexSpec, MetaData}, + {gen_key_in_bucket(PK), gen_val(), [{add, Cat, gen_index_value()} || Cat <- Categories ], []}, + case Tag of + ?STD_TAG -> [Pid, Bucket, Key, Value, IndexSpec, elements([none, Tag])]; + ?RIAK_TAG -> + Obj = testutil:riak_object(Bucket, Key, Value, MetaData), + [Pid, Bucket, Key, Obj, IndexSpec, Tag] + end)). + +put_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Value, _, _]) -> + Pid == Leveled. + +put_adapt(#{leveled := Leveled}, [_, Bucket, Key, Value, Spec, Tag]) -> + [ Leveled, Bucket, Key, Value, Spec, Tag ]. + +%% @doc put - The actual operation +put(Pid, Bucket, Key, Value, Spec, none) -> + leveled_bookie:book_put(Pid, Bucket, Key, Value, Spec); +put(Pid, Bucket, Key, Value, Spec, Tag) -> + leveled_bookie:book_put(Pid, Bucket, Key, Value, Spec, Tag). + +put_next(#{model := Model, previous_keys := PK} = S, _Value, [_Pid, Bucket, Key, Value, Spec, _Tag]) -> + ?CMD_VALID(S, put, + begin + NewSpec = + case orddict:find({Bucket, Key}, Model) of + error -> merge_index_spec([], Spec); + {ok, {_, OldSpec}} -> + merge_index_spec(OldSpec, Spec) + end, + S#{model => orddict:store({Bucket, Key}, {Value, NewSpec}, Model), + previous_keys => PK ++ [{Key, Bucket}]} + end, + S). + +put_post(S, [_, _, _, _, _, _], Res) -> + ?CMD_VALID(S, put, eq(Res, ok), eq(Res, {unsupported_message, put})). + +put_features(#{previous_keys := PK} = S, [_Pid, Bucket, Key, _Value, _, Tag], _Res) -> + ?CMD_VALID(S, put, + case + lists:member({Key, Bucket}, PK) of + true -> + [{put, update, Tag}]; + false -> + [{put, insert, Tag}] + end, + [{put, unsupported}]). + +merge_index_spec(Spec, []) -> + Spec; +merge_index_spec(Spec, [{add, Cat, Idx} | Rest]) -> + merge_index_spec(lists:delete({Cat, Idx}, Spec) ++ [{Cat, Idx}], Rest); +merge_index_spec(Spec, [{remove, Cat, Idx} | Rest]) -> + merge_index_spec(lists:delete({Cat, Idx}, Spec), Rest). + + +%% --- Operation: get --- +get_pre(S) -> + is_leveled_open(S). + +get_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> + ?LET({Key, Bucket}, gen_key_in_bucket(PK), + [Pid, Bucket, Key, case Tag of ?STD_TAG -> default(none, Tag); _ -> Tag end]). + +%% @doc get - The actual operation +get(Pid, Bucket, Key, none) -> + leveled_bookie:book_get(Pid, Bucket, Key); +get(Pid, Bucket, Key, Tag) -> + leveled_bookie:book_get(Pid, Bucket, Key, Tag). + +get_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Tag]) -> + Pid == Leveled. + +get_adapt(#{leveled := Leveled}, [_, Bucket, Key, Tag]) -> + [Leveled, Bucket, Key, Tag]. + +get_post(#{model := Model} = S, [_Pid, Bucket, Key, Tag], Res) -> + ?CMD_VALID(S, get, + case Res of + {ok, _} -> + {ok, {Value, _}} = orddict:find({Bucket, Key}, Model), + eq(Res, {ok, Value}); + not_found -> + %% Weird to be able to supply a tag, but must be STD_TAG... + Tag =/= ?STD_TAG orelse orddict:find({Bucket, Key}, Model) == error + end, + eq(Res, {unsupported_message, get})). + +get_features(#{deleted_keys := DK, previous_keys := PK}, [_Pid, Bucket, Key, _Tag], Res) -> + case Res of + not_found -> + [{get, not_found, deleted} || lists:member({Key, Bucket}, DK)] ++ + [{get, not_found, not_inserted} || not lists:member({Key, Bucket}, PK)]; + {ok, B} when is_binary(B) -> + [{get, found}]; + {unsupported_message, _} -> + [{get, unsupported}] + end. + +%% --- Operation: mput --- +mput_pre(S) -> + is_leveled_open(S). + +%% @doc mput_args - Argument generator +%% Specification says: duplicated should be removed +%% "%% The list should be de-duplicated before it is passed to the bookie." +%% Wether this means that keys should be unique or even Action and values is unclear. +%% Slack discussion: +%% `[{add, B1, K1, SK1}, {add, B1, K1, SK2}]` should be fine (same bucket and key, different subkey) +%% +%% Really weird to have to specify a value in case of a remove action +mput_args(#{leveled := Pid, previous_keys := PK}) -> + ?LET(Objs, list({gen_key_in_bucket(PK), nat()}), + [Pid, [ {weighted_default({5, add}, {1, remove}), Bucket, Key, SubKey, gen_val()} || {{Key, Bucket}, SubKey} <- Objs ]]). + + +mput_pre(#{leveled := Leveled}, [Pid, ObjSpecs]) -> + Pid == Leveled andalso no_key_dups(ObjSpecs) == ObjSpecs. + +mput_adapt(#{leveled := Leveled}, [_, ObjSpecs]) -> + [ Leveled, no_key_dups(ObjSpecs) ]. + +mput(Pid, ObjSpecs) -> + leveled_bookie:book_mput(Pid, ObjSpecs). + +mput_next(S, _, [_Pid, ObjSpecs]) -> + ?CMD_VALID(S, mput, + lists:foldl(fun({add, Bucket, Key, _SubKey, Value}, #{model := Model, previous_keys := PK} = Acc) -> + Acc#{model => orddict:store({Bucket, Key}, {Value, []}, Model), + previous_keys => PK ++ [{Key, Bucket}]}; + ({remove, Bucket, Key, _SubKey, _Value}, #{model := Model} = Acc) -> + Acc#{model => orddict:erase({Bucket, Key}, Model)} + end, S, ObjSpecs), + S). + +mput_post(S, [_, _], Res) -> + ?CMD_VALID(S, mput, eq(Res, ok), eq(Res, {unsupported_message, mput})). + +mput_features(S, [_Pid, ObjSpecs], _Res) -> + ?CMD_VALID(S, mput, + {mput, [ element(1, ObjSpec) || ObjSpec <- ObjSpecs ]}, + [{mput, unsupported}]). + +%% --- Operation: head --- +head_pre(S) -> + is_leveled_open(S). + +head_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> + ?LET({Key, Bucket}, gen_key_in_bucket(PK), + [Pid, Bucket, Key, Tag]). + +head_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Tag]) -> + Pid == Leveled. + +head_adapt(#{leveled := Leveled}, [_, Bucket, Key, Tag]) -> + [Leveled, Bucket, Key, Tag]. + +head(Pid, Bucket, Key, none) -> + leveled_bookie:book_head(Pid, Bucket, Key); +head(Pid, Bucket, Key, Tag) -> + leveled_bookie:book_head(Pid, Bucket, Key, Tag). + +head_post(#{model := Model} = S, [_Pid, Bucket, Key, Tag], Res) -> + ?CMD_VALID(S, head, + case Res of + {ok, _MetaData} -> + orddict:find({Bucket, Key}, Model) =/= error; + not_found -> + %% Weird to be able to supply a tag, but must be STD_TAG... + implies(lists:member(maps:get(start_opts, S), [{head_only, with_lookup}]), + lists:member(Tag, [?STD_TAG, none, ?HEAD_TAG])) orelse + orddict:find({Bucket, Key}, Model) == error; + {unsupported_message, head} -> + Tag =/= ?HEAD_TAG + end, + eq(Res, {unsupported_message, head})). + +head_features(#{deleted_keys := DK, previous_keys := PK}, [_Pid, Bucket, Key, _Tag], Res) -> + case Res of + not_found -> + [{head, not_found, deleted} || lists:member({Key, Bucket}, DK)] ++ + [{head, not_found, not_inserted} || not lists:member({Key, Bucket}, PK)]; + {ok, {_, _, _}} -> %% Metadata + [{head, found}]; + {ok, Bin} when is_binary(Bin) -> + [{head, found_riak_object}]; + {unsupported_message, _} -> + [{head, unsupported}] + end. + + +%% --- Operation: delete --- +delete_pre(S) -> + is_leveled_open(S). + +delete_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> + ?LET({Key, Bucket}, gen_key_in_bucket(PK), + [Pid, Bucket, Key, [], Tag]). + +delete_pre(#{leveled := Leveled, model := Model}, [Pid, Bucket, Key, Spec, _Tag]) -> + Pid == Leveled andalso + case orddict:find({Bucket, Key}, Model) of + error -> true; + {ok, {_, OldSpec}} -> + Spec == OldSpec + end. + +delete_adapt(#{leveled := Leveled, model := Model}, [_, Bucket, Key, Spec, Tag]) -> + NewSpec = + case orddict:find({Bucket, Key}, Model) of + error -> Spec; + {ok, {_, OldSpec}} -> + Spec == OldSpec + end, + [ Leveled, Bucket, Key, NewSpec, Tag ]. + +delete(Pid, Bucket, Key, Spec, ?STD_TAG) -> + leveled_bookie:book_delete(Pid, Bucket, Key, Spec); +delete(Pid, Bucket, Key, Spec, Tag) -> + leveled_bookie:book_put(Pid, Bucket, Key, delete, Spec, Tag). + +delete_next(#{model := Model, deleted_keys := DK} = S, _Value, [_Pid, Bucket, Key, _, _]) -> + ?CMD_VALID(S, delete, + S#{model => orddict:erase({Bucket, Key}, Model), + deleted_keys => DK ++ [{Key, Bucket} || orddict:is_key({Key, Bucket}, Model)]}, + S). + +delete_post(S, [_Pid, _Bucket, _Key, _, _], Res) -> + ?CMD_VALID(S, delete, + eq(Res, ok), + case Res of + {unsupported_message, _} -> true; + _ -> Res + end). + +delete_features(#{previous_keys := PK} = S, [_Pid, Bucket, Key, _, _], _Res) -> + ?CMD_VALID(S, delete, + case lists:member({Key, Bucket}, PK) of + true -> + [{delete, existing}]; + false -> + [{delete, none_existing}] + end, + [{delete, unsupported}]). + +%% --- Operation: is_empty --- +is_empty_pre(S) -> + is_leveled_open(S). + +is_empty_args(#{leveled := Pid, tag := Tag}) -> + [Pid, Tag]. + + +is_empty_pre(#{leveled := Leveled}, [Pid, _]) -> + Pid == Leveled. + +is_empty_adapt(#{leveled := Leveled}, [_, Tag]) -> + [Leveled, Tag]. + +%% @doc is_empty - The actual operation +is_empty(Pid, Tag) -> + leveled_bookie:book_isempty(Pid, Tag). + +is_empty_post(#{model := Model}, [_Pid, _Tag], Res) -> + Size = orddict:size(Model), + case Res of + true -> eq(0, Size); + false when Size == 0 -> expected_empty; + false when Size > 0 -> true + end. + +is_empty_features(_S, [_Pid, _], Res) -> + [{empty, Res}]. + +%% --- Operation: drop --- +drop_pre(S) -> + is_leveled_open(S). + +drop_args(#{leveled := Pid, dir := Dir} = S) -> + ?LET([Tag, _, Name], init_backend_args(S), + [Pid, Tag, [{root_path, Dir} | gen_opts()], Name]). + +drop_pre(#{leveled := Leveled} = S, [Pid, Tag, Opts, Name]) -> + Pid == Leveled andalso init_backend_pre(S, [Tag, Opts, Name]). + +drop_adapt(#{leveled := Leveled} = S, [_Pid, Tag, Opts, Name]) -> + [Leveled | init_backend_adapt(S, [Tag, Opts, Name])]. + +%% @doc drop - The actual operation +%% Remove fles from disk (directory structure may remain) and start a new clean database +drop(Pid, Tag, Opts, Name) -> + Mon = erlang:monitor(process, Pid), + ok = leveled_bookie:book_destroy(Pid), + receive + {'DOWN', Mon, _Type, Pid, _Info} -> + init_backend(Tag, Opts, Name) + after 5000 -> + {still_alive, Pid, Name} + end. + +drop_next(S, Value, [Pid, Tag, Opts, Name]) -> + S1 = stop_next(S, Value, [Pid]), + init_backend_next(S1#{model => orddict:new()}, + Value, [Tag, Opts, Name]). + +drop_post(_S, [_Pid, _Tag, _Opts, _], Res) -> + case is_pid(Res) of + true -> true; + false -> Res + end. + +drop_features(#{model := Model}, [_Pid, _Tag, _Opts, _], _Res) -> + Size = orddict:size(Model), + [{drop, empty} || Size == 0 ] ++ + [{drop, small} || Size > 0 andalso Size < 20 ] ++ + [{drop, medium} || Size >= 20 andalso Size < 1000 ] ++ + [{drop, large} || Size >= 1000 ]. + + + +%% --- Operation: kill --- +%% Test that killing the root Pid of leveled has the same effect as closing it nicely +%% that means, we don't loose data! Not even when parallel successful puts are going on. +kill_pre(S) -> + is_leveled_open(S). + +kill_args(#{leveled := Pid}) -> + [Pid]. + +kill_pre(#{leveled := Leveled}, [Pid]) -> + Pid == Leveled. + +kill_adapt(#{leveled := Leveled}, [_]) -> + [ Leveled ]. + +kill(Pid) -> + exit(Pid, kill), + timer:sleep(1). + +kill_next(S, Value, [Pid]) -> + stop_next(S, Value, [Pid]). + +%% Testing fold: +%% Note async and sync mode! +%% see https://github.com/martinsumner/riak_kv/blob/mas-2.2.5-tictactaae/src/riak_kv_leveled_backend.erl#L238-L419 + +%% --- Operation: index folding --- +indexfold_pre(S) -> + is_leveled_open(S). + +indexfold_args(#{leveled := Pid, counter := Counter, previous_keys := PK}) -> + ?LET({Key, Bucket}, gen_key_in_bucket(PK), + [Pid, default(Bucket, {Bucket, Key}), gen_foldacc(3), + ?LET({[N], M}, {gen_index_value(), choose(0,2)}, {gen_category(), [N], [N+M]}), + {bool(), + oneof([undefined, gen_index_value()])}, + Counter %% add a unique counter + ]). + +indexfold_pre(#{leveled := Leveled}, [Pid, _Constraint, _FoldAccT, _Range, _TermHandling, _Counter]) -> + %% Make sure we operate on an existing Pid when shrinking + %% Check start options validity as well? + Pid == Leveled. + +indexfold_adapt(#{leveled := Leveled}, [_, Constraint, FoldAccT, Range, TermHandling, Counter]) -> + %% Keep the counter! + [Leveled, Constraint, FoldAccT, Range, TermHandling, Counter]. + +indexfold(Pid, Constraint, FoldAccT, Range, {_, undefined} = TermHandling, _Counter) -> + {async, Folder} = leveled_bookie:book_indexfold(Pid, Constraint, FoldAccT, Range, TermHandling), + Folder; +indexfold(Pid, Constraint, FoldAccT, Range, {ReturnTerms, RegExp}, _Counter) -> + {ok, RE} = re:compile(RegExp), + {async, Folder} = leveled_bookie:book_indexfold(Pid, Constraint, FoldAccT, Range, {ReturnTerms, RE}), + Folder. + +indexfold_next(#{folders := Folders} = S, SymFolder, + [_, Constraint, {Fun, Acc}, {Category, From, To}, {ReturnTerms, RegExp}, Counter]) -> + ConstraintFun = + fun(B, K, Bool) -> + case Constraint of + {B, KStart} -> not Bool orelse K >= KStart; + B -> true; + _ -> false + end + end, + S#{folders => + Folders ++ + [#{counter => Counter, + type => indexfold, + folder => SymFolder, + reusable => true, + result => fun(Model) -> + Select = + lists:sort( + orddict:fold(fun({B, K}, {_V, Spec}, A) -> + [ {B, {Idx, K}} + || {Cat, Idx} <- Spec, + Idx >= From, Idx =< To, + Cat == Category, + ConstraintFun(B, K, Idx == From), + RegExp == undefined orelse string:find(Idx, RegExp) =/= nomatch + ] ++ A + end, [], Model)), + lists:foldl(fun({B, NK}, A) when ReturnTerms -> + Fun(B, NK, A); + ({B, {_, NK}}, A) -> + Fun(B, NK, A) + end, Acc, Select) + end + }], + counter => Counter + 1}. + +indexfold_post(_S, _, Res) -> + is_function(Res). + +indexfold_features(_S, [_Pid, Constraint, FoldAccT, _Range, {ReturnTerms, _}, _Counter], _Res) -> + [{foldAccT, FoldAccT}] ++ %% This will be extracted for printing later + [{index_fold, bucket} || not is_tuple(Constraint) ] ++ + [{index_fold, bucket_and_primary_key} || is_tuple(Constraint)] ++ + [{index_fold, return_terms, ReturnTerms} ]. + + + + +%% --- Operation: keylist folding --- +%% slack discussion: "`book_keylist` only passes `Bucket` and `Key` into the accumulator, ignoring SubKey - +%% so I don't think this can be used in head_only mode to return results that make sense" +%% +%% There are also keylist functions that take a specific bucket and range into account. Not considered yet. +keylistfold_pre(S) -> + is_leveled_open(S). + +keylistfold_args(#{leveled := Pid, counter := Counter, tag := Tag}) -> + [Pid, Tag, gen_foldacc(3), + Counter %% add a unique counter + ]. + +keylistfold_pre(#{leveled := Leveled}, [Pid, _Tag, _FoldAccT, _Counter]) -> + %% Make sure we operate on an existing Pid when shrinking + %% Check start options validity as well? + Pid == Leveled. + +keylistfold_adapt(#{leveled := Leveled}, [_, Tag, FoldAccT, Counter]) -> + %% Keep the counter! + [Leveled, Tag, FoldAccT, Counter]. + +keylistfold(Pid, Tag, FoldAccT, _Counter) -> + {async, Folder} = leveled_bookie:book_keylist(Pid, Tag, FoldAccT), + Folder. + +keylistfold_next(#{folders := Folders, model := Model} = S, SymFolder, + [_, _Tag, {Fun, Acc}, Counter]) -> + S#{folders => + Folders ++ + [#{counter => Counter, + type => keylist, + folder => SymFolder, + reusable => false, + result => fun(_) -> orddict:fold(fun({B, K}, _V, A) -> Fun(B, K, A) end, Acc, Model) end + }], + counter => Counter + 1}. + +keylistfold_post(_S, _, Res) -> + is_function(Res). + +keylistfold_features(_S, [_Pid, _Tag, FoldAccT, _Counter], _Res) -> + [{foldAccT, FoldAccT}]. %% This will be extracted for printing later + + +%% --- Operation: bucketlistfold --- +bucketlistfold_pre(S) -> + is_leveled_open(S). + +bucketlistfold_args(#{leveled := Pid, counter := Counter, tag := Tag}) -> + [Pid, Tag, gen_foldacc(2), elements([first, all]), Counter]. + +bucketlistfold_pre(#{leveled := Leveled}, [Pid, _Tag, _FoldAccT, _Constraints, _]) -> + Pid == Leveled. + +bucketlistfold_adapt(#{leveled := Leveled}, [_Pid, Tag, FoldAccT, Constraints, Counter]) -> + [Leveled, Tag, FoldAccT, Constraints, Counter]. + +bucketlistfold(Pid, Tag, FoldAccT, Constraints, _) -> + {async, Folder} = leveled_bookie:book_bucketlist(Pid, Tag, FoldAccT, Constraints), + Folder. + +bucketlistfold_next(#{folders := Folders} = S, SymFolder, + [_, _, {Fun, Acc}, Constraints, Counter]) -> + S#{folders => + Folders ++ + [#{counter => Counter, + type => bucketlist, + folder => SymFolder, + reusable => true, + result => fun(Model) -> + Bs = orddict:fold(fun({B, _K}, _V, A) -> A ++ [B || not lists:member(B, A)] end, [], Model), + case {Constraints, Bs} of + {all, _} -> + lists:foldl(fun(B, A) -> Fun(B, A) end, Acc, Bs); + {first, []} -> + Acc; + {first, [First|_]} -> + lists:foldl(fun(B, A) -> Fun(B, A) end, Acc, [First]) + end + end + }], + counter => Counter + 1}. + +bucketlistfold_post(_S, [_Pid, _Tag, _FoldAccT, _Constraints, _], Res) -> + is_function(Res). + +bucketlistfold_features(_S, [_Pid, _Tag, FoldAccT, _Constraints, _], _Res) -> + [ {foldAccT, FoldAccT} ]. + +%% --- Operation: objectfold --- +objectfold_pre(S) -> + is_leveled_open(S). + +objectfold_args(#{leveled := Pid, counter := Counter, tag := Tag}) -> + [Pid, Tag, gen_foldacc(4), bool(), Counter]. + +objectfold_pre(#{leveled := Leveled}, [Pid, _Tag, _FoldAccT, _Snapshot, _Counter]) -> + Leveled == Pid. + +objectfold_adapt(#{leveled := Leveled}, [_Pid, Tag, FoldAccT, Snapshot, Counter]) -> + [Leveled, Tag, FoldAccT, Snapshot, Counter]. + +objectfold(Pid, Tag, FoldAccT, Snapshot, _Counter) -> + {async, Folder} = leveled_bookie:book_objectfold(Pid, Tag, FoldAccT, Snapshot), + Folder. + +objectfold_next(#{folders := Folders, model := Model} = S, SymFolder, + [_Pid, _Tag, {Fun, Acc}, Snapshot, Counter]) -> + S#{folders => + Folders ++ + [#{counter => Counter, + type => objectfold, + folder => SymFolder, + reusable => not Snapshot, + result => fun(M) -> + OnModel = + case Snapshot of + true -> Model; + false -> M + end, + Objs = orddict:fold(fun({B, K}, {V, _}, A) -> [{B, K, V} | A] end, [], OnModel), + lists:foldr(fun({B, K, V}, A) -> Fun(B, K, V, A) end, Acc, Objs) + end + }], + counter => Counter + 1}. + +objectfold_post(_S, [_Pid, _Tag, _FoldAccT, _Snapshot, _Counter], Res) -> + is_function(Res). + +objectfold_features(_S, [_Pid, _Tag, FoldAccT, _Snapshot, _Counter], _Res) -> + [{foldAccT, FoldAccT}]. %% This will be extracted for printing later + + + + +%% --- Operation: fold_run --- +fold_run_pre(S) -> + maps:get(folders, S, []) =/= []. + +fold_run_args(#{folders := Folders}) -> + ?LET(#{counter := Counter, folder := Folder}, elements(Folders), + [Counter, Folder]). + +fold_run_pre(#{folders := Folders}, [Counter, _Folder]) -> + %% Ensure membership even under shrinking + %% Counter is fixed at first generation and does not shrink! + get_foldobj(Folders, Counter) =/= undefined. + +fold_run(_, Folder) -> + catch Folder(). + +fold_run_next(#{folders := Folders} = S, _Value, [Counter, _Folder]) -> + %% leveled_runner comment: "Iterators should de-register themselves from the Penciller on completion." + FoldObj = get_foldobj(Folders, Counter), + case FoldObj of + #{reusable := false} -> + UsedFolders = maps:get(used_folders, S, []), + S#{folders => Folders -- [FoldObj], + used_folders => UsedFolders ++ [FoldObj]}; + _ -> + S + end. + +fold_run_post(#{folders := Folders, leveled := Leveled, model := Model}, [Count, _], Res) -> + case Leveled of + undefined -> + is_exit(Res); + _ -> + #{result := ResFun} = get_foldobj(Folders, Count), + eq(Res, ResFun(Model)) + end. + +fold_run_features(#{folders := Folders, leveled := Leveled}, [Count, _Folder], Res) -> + #{type := Type} = get_foldobj(Folders, Count), + [ {fold_run, Type} || Leveled =/= undefined ] ++ + [ fold_run_on_stopped_leveled || Leveled == undefined ] ++ + [ {fold_run, found_list, length(Res)}|| is_list(Res) ] ++ + [ {fold_run, found_integer}|| is_integer(Res) ]. + + +%% --- Operation: fold_run on already used folder --- +%% A fold that has already ran to completion should results in an exception when re-used. +%% leveled_runner comment: "Iterators should de-register themselves from the Penciller on completion." +noreuse_fold_pre(S) -> + maps:get(used_folders, S, []) =/= []. + +noreuse_fold_args(#{used_folders := Folders}) -> + ?LET(#{counter := Counter, folder := Folder}, elements(Folders), + [Counter, Folder]). + +noreuse_fold_pre(S, [Counter, _Folder]) -> + %% Ensure membership even under shrinking + %% Counter is fixed at first generation and does not shrink! + lists:member(Counter, + [ maps:get(counter, Used) || Used <- maps:get(used_folders, S, []) ]). + +noreuse_fold(_, Folder) -> + catch Folder(). + +noreuse_fold_post(_S, [_, _], Res) -> + is_exit(Res). + +noreuse_fold_features(_, [_, _], _) -> + [ reuse_fold ]. + + +%% --- Operation: fold_run on folder that survived a crash --- +%% A fold that has already ran to completion should results in an exception when re-used. +stop_fold_pre(S) -> + maps:get(stop_folders, S, []) =/= []. + +stop_fold_args(#{stop_folders := Folders}) -> + ?LET(#{counter := Counter, folder := Folder}, elements(Folders), + [Counter, Folder]). + +stop_fold_pre(S, [Counter, _Folder]) -> + %% Ensure membership even under shrinking + %% Counter is fixed at first generation and does not shrink! + lists:member(Counter, + [ maps:get(counter, Used) || Used <- maps:get(stop_folders, S, []) ]). + +stop_fold(_, Folder) -> + catch Folder(). + +stop_fold_post(_S, [_Counter, _], Res) -> + is_exit(Res). + +stop_fold_features(S, [_, _], _) -> + [ case maps:get(leveled, S) of + undefined -> + stop_fold_when_closed; + _ -> + stop_fold_when_open + end ]. + + +weight(#{previous_keys := []}, Command) when Command == get; + Command == delete -> + 1; +weight(S, C) when C == get; + C == put -> + ?CMD_VALID(S, put, 10, 1); +weight(_S, stop) -> + 1; +weight(_, _) -> + 1. + + +is_valid_cmd(S, put) -> + not in_head_only_mode(S); +is_valid_cmd(S, delete) -> + is_valid_cmd(S, put); +is_valid_cmd(S, get) -> + not in_head_only_mode(S); +is_valid_cmd(S, head) -> + not lists:member({head_only, no_lookup}, maps:get(start_opts, S, [])); +is_valid_cmd(S, mput) -> + in_head_only_mode(S). + + + +%% @doc check that the implementation of leveled is equivalent to a +%% sorted dict at least +-spec prop_db() -> eqc:property(). +prop_db() -> + Dir = "./leveled_data", + eqc:dont_print_counterexample( + ?LET(Shrinking, parameter(shrinking, false), + ?FORALL({Kind, Cmds}, oneof([{seq, more_commands(20, commands(?MODULE))}, + {par, more_commands(2, parallel_commands(?MODULE))}]), + begin + delete_level_data(Dir), + ?IMPLIES(empty_dir(Dir), + ?ALWAYS(if Shrinking -> 10; true -> 1 end, + begin + Procs = erlang:processes(), + StartTime = erlang:system_time(millisecond), + + RunResult = execute(Kind, Cmds, [{dir, Dir}]), + %% Do not extract the 'state' from this tuple, since parallel commands + %% miss the notion of final state. + CallFeatures = [ Feature || Feature <- call_features(history(RunResult)), + not is_foldaccT(Feature), + not (is_tuple(Feature) andalso element(1, Feature) == start_options) + ], + StartOptionFeatures = [ lists:keydelete(root_path, 1, Feature) || {start_options, Feature} <- call_features(history(RunResult)) ], + + case whereis(maps:get(sut, initial_state())) of + undefined -> delete_level_data(Dir); + Pid when is_pid(Pid) -> + leveled_bookie:book_destroy(Pid) + end, + + Wait = wait_for_procs(Procs, 1500), + RunTime = erlang:system_time(millisecond) - StartTime, + + %% Since in parallel commands we don't have access to the state, we retrieve functions + %% from the features + FoldAccTs = [ FoldAccT || Entry <- history(RunResult), + {foldAccT, FoldAccT} <- eqc_statem:history_features(Entry)], + + pretty_commands(?MODULE, Cmds, RunResult, + measure(time_per_test, RunTime, + aggregate(command_names(Cmds), + collect(Kind, + aggregate(with_title('Features'), CallFeatures, + aggregate(with_title('Start Options'), StartOptionFeatures, + features(CallFeatures, + conjunction([{result, + ?WHENFAIL([ begin + eqc:format("~p with acc ~p:\n~s\n", [F, Acc, + show_function(F)]) + end || {F, Acc} <- FoldAccTs ], + result(RunResult) == ok)}, + {data_cleanup, + ?WHENFAIL(eqc:format("~s\n", [os:cmd("ls -Rl " ++ Dir)]), + empty_dir(Dir))}, + {pid_cleanup, equals(Wait, [])}])))))))) + end)) + end))). + +history({H, _, _}) -> H. +result({_, _, Res}) -> Res. + +execute(seq, Cmds, Env) -> + run_commands(Cmds, Env); +execute(par, Cmds, Env) -> + run_parallel_commands(Cmds, Env). + +is_exit({'EXIT', _}) -> + true; +is_exit(Other) -> + {expected_exit, Other}. + +is_foldaccT({foldAccT, _}) -> + true; +is_foldaccT(_) -> + false. + +show_function(F) -> + case proplists:get_value(module, erlang:fun_info(F)) of + eqc_fun -> + eqc_fun:show_function(F); + _ -> + proplists:get_value(name, erlang:fun_info(F)) + end. + + +%% slack discussion: +%% `max_journalsize` should be at least 2048 + byte_size(smallest_object) + byte_size(smallest_object's key) + overhead (which is a few bytes per K/V pair). +gen_opts() -> + options([%% {head_only, elements([false, no_lookup, with_lookup])} we don't test head_only mode + {compression_method, elements([native, lz4])} + , {compression_point, elements([on_compact, on_receipt])} + %% , {max_journalsize, ?LET(N, nat(), 2048 + 1000 + 32 + 16 + 16 + N)} + , {cache_size, oneof([nat(), 2048, 2060, 5000])} + ]). + +options(GenList) -> + ?LET(Bools, vector(length(GenList), bool()), + [ Opt || {Opt, true} <- lists:zip(GenList, Bools)]). + + +gen_key() -> + binary(16). + +%% Cannot be atoms! +%% key() type specified: should be binary(). +gen_bucket() -> + elements([<<"bucket1">>, <<"bucket2">>, <<"bucket3">>]). + +gen_val() -> + noshrink(binary(32)). + +gen_categories(?RIAK_TAG) -> + sublist(categories()); +gen_categories(_) -> + %% for ?STD_TAG this seems to make little sense + []. + + +categories() -> + [dep, lib]. + +gen_category() -> + elements(categories()). + +gen_index_value() -> + %% Carefully selected to have one out-layer and several border cases. + [elements("arts")]. + +gen_key_in_bucket([]) -> + {gen_key(), gen_bucket()}; +gen_key_in_bucket(Previous) -> + ?LET({K, B}, elements(Previous), + frequency([{1, gen_key_in_bucket([])}, + {1, {K, gen_bucket()}}, + {2, {K, B}}])). + +gen_foldacc(2) -> + ?SHRINK(oneof([{eqc_fun:function2(int()), int()}, + {eqc_fun:function2(list(int())), list(int())}]), + [fold_buckets()]); +gen_foldacc(3) -> + ?SHRINK(oneof([{eqc_fun:function3(int()), int()}, + {eqc_fun:function3(list(int())), list(int())}]), + [fold_collect()]); +gen_foldacc(4) -> + ?SHRINK(oneof([{eqc_fun:function4(int()), int()}, + {eqc_fun:function4(list(int())), list(int())}]), + [fold_objects()]). + + + +fold_buckets() -> + {fun(B, Acc) -> [B | Acc] end, []}. + +fold_collect() -> + {fun(X, Y, Acc) -> [{X, Y} | Acc] end, []}. + +fold_objects() -> + {fun(X, Y, Z, Acc) -> [{X, Y, Z} | Acc] end, []}. + +%% This makes system fall over +fold_collect_no_acc() -> + fun(X, Y, Z) -> [{X, Y} | Z] end. + +fold_count() -> + {fun(_X, _Y, Z) -> Z + 1 end, 0}. + +fold_keys() -> + {fun(X, _Y, Z) -> [X | Z] end, []}. + + +empty_dir(Dir) -> + case file:list_dir(Dir) of + {error, enoent} -> true; + {ok, Ds} -> + lists:all(fun(D) -> empty_dir(filename:join(Dir, D)) end, Ds); + _ -> + false + end. + +get_foldobj([], _Counter) -> + undefined; +get_foldobj([#{counter := Counter} = Map | _Rest], Counter) -> + Map; +get_foldobj([_ | Rest], Counter) -> + get_foldobj(Rest, Counter). + + +%% Helper for all those preconditions that just check that leveled Pid +%% is populated in state. (We cannot check with is_pid, since that's +%% symbolic in test case generation!). +is_leveled_open(S) -> + maps:get(leveled, S, undefined) =/= undefined. + +in_head_only_mode(S) -> + proplists:get_value(head_only, maps:get(start_opts, S, []), false) =/= false. + +wait_for_procs(Known, Timeout) -> + case erlang:processes() -- Known of + [] -> []; + Running -> + case Timeout > 0 of + true -> + timer:sleep(100), + wait_for_procs(Known, Timeout - 100); + false -> + Running + end + end. + +delete_level_data(Dir) -> + os:cmd("rm -rf " ++ Dir). + +%% Slack discussion: +%% `[{add, B1, K1, SK1}, {add, B1, K1, SK2}]` should be fine (same bucket and key, different subkey) +no_key_dups([]) -> + []; +no_key_dups([{_Action, Bucket, Key, SubKey, _Value} = E | Es]) -> + [E | no_key_dups([ {A, B, K, SK, V} || {A, B, K, SK, V} <- Es, + {B, K, SK} =/= {Bucket, Key, SubKey}])]. \ No newline at end of file From a13a6ae45fac09167fc32250018e29c96fa9ea34 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Tue, 22 Jan 2019 12:53:31 +0000 Subject: [PATCH 02/15] Updated model This has inappropriate default parameter changes. --- .gitignore | 3 + current_counterexample.eqc | Bin 1034 -> 217582 bytes src/leveled_bookie.erl | 16 ++- src/leveled_iclerk.erl | 1 + src/leveled_inker.erl | 4 +- src/leveled_sst.erl | 2 +- test/leveledjc_eqc.erl | 228 ++++++++++++++++++++++++++++++++++--- 7 files changed, 231 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 8031a86..b266ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .DS_Store rebar.lock test/test_area/* +cover +cover_* +.eqc-info diff --git a/current_counterexample.eqc b/current_counterexample.eqc index 775fd177f75722d4ee2fa79f960f1545d15c6e25..c4c9f59c315444d99fded54af30aa076bddbfa3e 100644 GIT binary patch literal 217582 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 diff --git a/src/leveled_bookie.erl b/src/leveled_bookie.erl index 73c0c33..cfa4084 100644 --- a/src/leveled_bookie.erl +++ b/src/leveled_bookie.erl @@ -62,6 +62,7 @@ book_headonly/4, book_snapshot/4, book_compactjournal/2, + book_eqccompactjournal/2, book_islastcompactionpending/1, book_trimjournal/1, book_hotbackup/1, @@ -100,8 +101,8 @@ -include_lib("eunit/include/eunit.hrl"). -define(CACHE_SIZE, 2500). --define(MIN_CACHE_SIZE, 100). --define(MIN_PCL_CACHE_SIZE, 400). +-define(MIN_CACHE_SIZE, 1). +-define(MIN_PCL_CACHE_SIZE, 4). -define(MAX_PCL_CACHE_SIZE, 28000). % This is less than actual max - but COIN_SIDECOUNT -define(CACHE_SIZE_JITTER, 25). @@ -1005,6 +1006,7 @@ book_snapshot(Pid, SnapType, Query, LongRunning) -> -spec book_compactjournal(pid(), integer()) -> ok. +-spec book_eqccompactjournal(pid(), integer()) -> {ok, pid()}. -spec book_islastcompactionpending(pid()) -> boolean(). -spec book_trimjournal(pid()) -> ok. @@ -1013,9 +1015,13 @@ book_snapshot(Pid, SnapType, Query, LongRunning) -> %% the scheduling of Journla compaction is called externally, so it is assumed %% in Riak it will be triggered by a vnode callback. -book_compactjournal(Pid, Timeout) -> +book_eqccompactjournal(Pid, Timeout) -> gen_server:call(Pid, {compact_journal, Timeout}, infinity). +book_compactjournal(Pid, Timeout) -> + {ok, _P} = gen_server:call(Pid, {compact_journal, Timeout}, infinity), + ok. + %% @doc Check on progress of the last compaction book_islastcompactionpending(Pid) -> @@ -1371,10 +1377,10 @@ handle_call({return_runner, QueryType}, _From, State) -> fold_countdown = CountDown}}; handle_call({compact_journal, Timeout}, _From, State) when State#state.head_only == false -> - ok = leveled_inker:ink_compactjournal(State#state.inker, + R = leveled_inker:ink_compactjournal(State#state.inker, self(), Timeout), - {reply, ok, State}; + {reply, R, State}; handle_call(confirm_compact, _From, State) when State#state.head_only == false -> {reply, leveled_inker:ink_compactionpending(State#state.inker), State}; diff --git a/src/leveled_iclerk.erl b/src/leveled_iclerk.erl index 5f6b09a..f0a2318 100644 --- a/src/leveled_iclerk.erl +++ b/src/leveled_iclerk.erl @@ -182,6 +182,7 @@ clerk_hashtablecalc(HashTree, StartPos, CDBpid) -> %% @doc %% Stop the clerk clerk_stop(Pid) -> + unlink(Pid), gen_server:cast(Pid, stop). -spec clerk_loglevel(pid(), leveled_log:log_level()) -> ok. diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index 3f27e31..cee237a 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -348,7 +348,7 @@ ink_loadpcl(Pid, MinSQN, FilterFun, Penciller) -> as_ink}, infinity). --spec ink_compactjournal(pid(), pid(), integer()) -> ok. +-spec ink_compactjournal(pid(), pid(), integer()) -> {ok, pid()}. %% @doc %% Trigger a compaction event. the compaction event will use a sqn check %% against the Ledger to see if a value can be compacted - if the penciller @@ -612,7 +612,7 @@ handle_call({compact, FilterFun, self(), Timeout), - {reply, ok, State#state{compaction_pending=true}}; + {reply, {ok, State#state.clerk}, State#state{compaction_pending=true}}; handle_call(compaction_complete, _From, State) -> {reply, ok, State#state{compaction_pending=false}}; handle_call(compaction_pending, _From, State) -> diff --git a/src/leveled_sst.erl b/src/leveled_sst.erl index d86f9c5..efe4441 100644 --- a/src/leveled_sst.erl +++ b/src/leveled_sst.erl @@ -72,7 +72,7 @@ -include("include/leveled.hrl"). --define(MAX_SLOTS, 256). +-define(MAX_SLOTS, 2). -define(LOOK_SLOTSIZE, 128). % Maximum of 128 -define(LOOK_BLOCKSIZE, {24, 32}). % 4x + y = ?LOOK_SLOTSIZE -define(NOLOOK_SLOTSIZE, 256). diff --git a/test/leveledjc_eqc.erl b/test/leveledjc_eqc.erl index eec9a64..18c80c8 100644 --- a/test/leveledjc_eqc.erl +++ b/test/leveledjc_eqc.erl @@ -24,7 +24,8 @@ -include_lib("eunit/include/eunit.hrl"). -include("../include/leveled.hrl"). --compile([export_all, nowarn_export_all]). +-compile([export_all, nowarn_export_all, {nowarn_deprecated_function, + [{gen_fsm, send_event, 2}]}]). -define(NUMTESTS, 1000). -define(QC_OUT(P), @@ -75,7 +76,7 @@ init_backend_args(#{dir := Dir, sut := Name} = S) -> case maps:get(start_opts, S, undefined) of undefined -> [ default(?RIAK_TAG, ?STD_TAG), %% Just test one tag at a time - [{root_path, Dir}, {log_level, error} | gen_opts()], Name ]; + [{root_path, Dir}, {log_level, error}, {cache_size, 10}, {max_pencillercachesize, 40}, {max_journalsize, 20000} | gen_opts()], Name ]; Opts -> %% root_path is part of existing options [ maps:get(tag, S), Opts, Name ] @@ -93,6 +94,7 @@ init_backend_adapt(S, [Tag, Options, Name]) -> %% @doc init_backend - The actual operation %% Start the database and read data from disk init_backend(_Tag, Options, Name) -> + % Options0 = proplists:delete(log_level, Options), case leveled_bookie:book_start(Options) of {ok, Bookie} -> unlink(Bookie), @@ -133,9 +135,10 @@ stop(Pid) -> stop_next(S, _Value, [_Pid]) -> S#{leveled => undefined, - folders => [], - used_folders => [], - stop_folders => maps:get(folders, S, []) ++ maps:get(used_folders, S, [])}. + iclerk => undefined, + folders => [], + used_folders => [], + stop_folders => maps:get(folders, S, []) ++ maps:get(used_folders, S, [])}. stop_post(_S, [Pid], _Res) -> Mon = erlang:monitor(process, Pid), @@ -147,6 +150,75 @@ stop_post(_S, [Pid], _Res) -> end. +%% --- Operation: updateload --- +updateload_pre(S) -> + is_leveled_open(S). + +%% updateload for specific bucket (doesn't overlap with get/put/delete) +updateload_args(#{leveled := Pid, tag := Tag}) -> + ?LET(Categories, gen_categories(Tag), + ?LET({{Key, Bucket}, Value, IndexSpec, MetaData}, + {{gen_key(), <<"LoadBucket">>}, gen_val(), [{add, Cat, gen_index_value()} || Cat <- Categories ], []}, + case Tag of + ?STD_TAG -> [Pid, Bucket, Key, Value, Value, IndexSpec, Tag, MetaData]; + ?RIAK_TAG -> + Obj = testutil:riak_object(Bucket, Key, Value, MetaData), %% this has a side effect inserting a random nr + [Pid, Bucket, Key, Value, Obj, IndexSpec, Tag, MetaData] + end + )). + +updateload_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Value, _Obj, _, _, _]) -> + Pid == Leveled. + +updateload_adapt(#{leveled := Leveled}, [_, Bucket, Key, Value, Obj, Spec, Tag, MetaData]) -> + [ Leveled, Bucket, Key, Value, Obj, Spec, Tag, MetaData ]. + +%% @doc put - The actual operation +updateload(Pid, Bucket, Key, Value, Obj, Spec, Tag, MetaData) -> + Values = + case Tag of + ?STD_TAG -> multiply(100, Value); + ?RIAK_TAG -> + lists:map(fun(V) -> testutil:riak_object(Bucket, Key, V, MetaData) end, + multiply(100, Value)) + end ++ [Obj], + lists:map(fun(V) -> leveled_bookie:book_put(Pid, Bucket, Key, V, Spec, Tag) end, Values). + +multiply(1, _Value) -> + []; +multiply(N, Value) when N > 1 -> + <> = Value, + NewValue = <>, + [NewValue | multiply(N-1, NewValue)]. + +updateload_next(#{model := Model} = S, _V, [_Pid, Bucket, Key, _Value, Obj, Spec, _Tag, _MetaData]) -> + ?CMD_VALID(S, put, + begin + NewSpec = + case orddict:find({Bucket, Key}, Model) of + error -> merge_index_spec([], Spec); + {ok, {_, OldSpec}} -> + merge_index_spec(OldSpec, Spec) + end, + S#{model => orddict:store({Bucket, Key}, {Obj, NewSpec}, Model)} + end, + S). + +updateload_post(S, [_, _, _, _, _, _, _, _], Results) -> + lists:all(fun(Res) -> ?CMD_VALID(S, put, Res == ok, Res == {unsupported_message, put}) end, Results). + +updateload_features(#{previous_keys := PK} = S, [_Pid, Bucket, Key, _Value, _Obj, _, Tag, _], _Res) -> + ?CMD_VALID(S, put, + case + lists:member({Key, Bucket}, PK) of + true -> + [{updateload, update, Tag}]; + false -> + [{updateload, insert, Tag}] + end, + [{updateload, unsupported}]). + + %% --- Operation: put --- put_pre(S) -> is_leveled_open(S). @@ -496,6 +568,113 @@ kill(Pid) -> kill_next(S, Value, [Pid]) -> stop_next(S, Value, [Pid]). + + +%% --- Operation: compactisalive --- + +compactisalive_pre(S) -> + is_leveled_open(S) andalso maps:get(iclerk, S, undefined) /= undefined. + +compactisalive_args(#{iclerk := IClerk}) -> + [IClerk]. + +compactisalive_pre(#{iclerk := Pid}, [IClerk]) -> + Pid == IClerk. + +compactisalive(IClerk) -> + is_process_alive(IClerk). + +compactisalive_post(_S, [_IClerk], Res) -> + Res. + + +%% --- Operation: compacthappened --- + +compacthappened_pre(S) -> + is_leveled_open(S) andalso maps:get(iclerk, S, undefined) /= undefined. + +%% Commenting out args disables the operation +%% compacthappened_args(#{dir := DataDir}) -> +%% [DataDir]. + +compacthappened(DataDir) -> + PostCompact = filename:join(DataDir, "journal/journal_files/post_compact"), + case filelib:is_dir(PostCompact) of + true -> + {ok, Files} = file:list_dir(PostCompact), + Files; + false -> + [] + end. + +ledgerpersisted(DataDir) -> + LedgerPath = filename:join(DataDir, "ledger/ledger_files"), + case filelib:is_dir(LedgerPath) of + true -> + {ok, Files} = file:list_dir(LedgerPath), + Files; + false -> + [] + end. + +journalwritten(DataDir) -> + JournalPath = filename:join(DataDir, "journal/journal_files"), + case filelib:is_dir(JournalPath) of + true -> + {ok, Files} = file:list_dir(JournalPath), + Files; + false -> + [] + end. + +compacthappened_post(_S, [_DataDir], Res) -> + eq(Res, []). + + + +%% --- Operation: compact journal --- + +compact_pre(S) -> + is_leveled_open(S). + +compact_args(#{leveled := Pid}) -> + [Pid, nat()]. + +compact_pre(#{leveled := Leveled}, [Pid, _TS]) -> + Pid == Leveled. + +compact_adapt(#{leveled := Leveled}, [_Pid, TS]) -> + [ Leveled, TS ]. + +compact(Pid, TS) -> + {ok, IClerk} = leveled_bookie:book_eqccompactjournal(Pid, TS), + IClerk. + +compact_next(S, IClerk, [_Pid, _TS]) -> + case maps:get(iclerk, S, undefined) of + undefined -> + S#{iclerk => IClerk}; + _ -> + S + end. + +compact_post(S, [_Pid, _TS], Res) -> + case maps:get(iclerk, S, undefined) of + undefined -> + is_pid(Res); + IClerk -> + IClerk == Res + end. + +compact_features(S, [_Pid, _TS], _Res) -> + case maps:get(iclerk, S, undefined) of + undefined -> + [{compact, fresh}]; + _ -> + [{compact, repeat}] + end. + + %% Testing fold: %% Note async and sync mode! %% see https://github.com/martinsumner/riak_kv/blob/mas-2.2.5-tictactaae/src/riak_kv_leveled_backend.erl#L238-L419 @@ -816,11 +995,14 @@ stop_fold_features(S, [_, _], _) -> end ]. -weight(#{previous_keys := []}, Command) when Command == get; - Command == delete -> +weight(#{previous_keys := []}, get) -> + 1; +weight(#{previous_keys := []}, delete) -> 1; weight(S, C) when C == get; - C == put -> + C == put; + C == delete; + C == updateload -> ?CMD_VALID(S, put, 10, 1); weight(_S, stop) -> 1; @@ -849,7 +1031,8 @@ prop_db() -> eqc:dont_print_counterexample( ?LET(Shrinking, parameter(shrinking, false), ?FORALL({Kind, Cmds}, oneof([{seq, more_commands(20, commands(?MODULE))}, - {par, more_commands(2, parallel_commands(?MODULE))}]), + {par, more_commands(2, parallel_commands(?MODULE))} + ]), begin delete_level_data(Dir), ?IMPLIES(empty_dir(Dir), @@ -867,6 +1050,13 @@ prop_db() -> ], StartOptionFeatures = [ lists:keydelete(root_path, 1, Feature) || {start_options, Feature} <- call_features(history(RunResult)) ], + timer:sleep(1000), + CompactionFiles = compacthappened(Dir), + LedgerFiles = ledgerpersisted(Dir), + JournalFiles = journalwritten(Dir), + io:format("File counts: Compacted ~w Journal ~w Ledger ~w~n", [length(CompactionFiles), length(LedgerFiles), length(JournalFiles)]), + + case whereis(maps:get(sut, initial_state())) of undefined -> delete_level_data(Dir); Pid when is_pid(Pid) -> @@ -885,6 +1075,9 @@ prop_db() -> measure(time_per_test, RunTime, aggregate(command_names(Cmds), collect(Kind, + measure(compaction_files, length(CompactionFiles), + measure(ledger_files, length(LedgerFiles), + measure(journal_files, length(JournalFiles), aggregate(with_title('Features'), CallFeatures, aggregate(with_title('Start Options'), StartOptionFeatures, features(CallFeatures, @@ -897,7 +1090,7 @@ prop_db() -> {data_cleanup, ?WHENFAIL(eqc:format("~s\n", [os:cmd("ls -Rl " ++ Dir)]), empty_dir(Dir))}, - {pid_cleanup, equals(Wait, [])}])))))))) + {pid_cleanup, equals(Wait, [])}]))))))))))) end)) end))). @@ -935,7 +1128,7 @@ gen_opts() -> {compression_method, elements([native, lz4])} , {compression_point, elements([on_compact, on_receipt])} %% , {max_journalsize, ?LET(N, nat(), 2048 + 1000 + 32 + 16 + 16 + N)} - , {cache_size, oneof([nat(), 2048, 2060, 5000])} + %% , {cache_size, oneof([nat(), 2048, 2060, 5000])} ]). options(GenList) -> @@ -952,7 +1145,7 @@ gen_bucket() -> elements([<<"bucket1">>, <<"bucket2">>, <<"bucket3">>]). gen_val() -> - noshrink(binary(32)). + noshrink(binary(256)). gen_categories(?RIAK_TAG) -> sublist(categories()); @@ -1044,11 +1237,16 @@ wait_for_procs(Known, Timeout) -> case erlang:processes() -- Known of [] -> []; Running -> - case Timeout > 0 of - true -> + if + Timeout > 100 -> timer:sleep(100), wait_for_procs(Known, Timeout - 100); - false -> + Timeout >= 0 -> + lists:map(fun(P) -> io:format("********* Sending timeout to ~w *********~n", [P]), gen_fsm:send_event(P, timeout) end, Running), + timer:sleep(100), + wait_for_procs(Known, Timeout - 100); + true -> + lists:foreach(fun(P) -> io:format("Process info : ~w~n", [process_info(P)]) end, Running), Running end end. From c9bf43953b5488a45ee7e9d54a45cbe27036ab16 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Thu, 24 Jan 2019 14:32:01 +0000 Subject: [PATCH 03/15] Expect TS in snapshot references from manifest Add type to prevent re-occurence. This is also detected by failure in eqc tests. --- src/leveled_inker.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index 2faadf6..d2ad02b 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -142,7 +142,7 @@ journal_sqn = 0 :: integer(), active_journaldb :: pid() | undefined, pending_removals = [] :: list(), - registered_snapshots = [] :: list(), + registered_snapshots = [] :: list(registered_snapshot()), root_path :: string() | undefined, cdb_options :: #cdb_options{} | undefined, clerk :: pid() | undefined, @@ -157,7 +157,7 @@ -type inker_options() :: #inker_options{}. -type ink_state() :: #state{}. - +-type registered_snapshot() :: {pid(), os:timestamp(), integer()}. %%%============================================================================ %%% API @@ -843,11 +843,12 @@ start_from_file(InkOpts) -> clerk = Clerk}}. --spec shutdown_snapshots(list(tuple())) -> ok. +-spec shutdown_snapshots(list(registered_snapshot())) -> ok. %% @doc %% Shutdown any snapshots before closing the store shutdown_snapshots(Snapshots) -> - lists:foreach(fun({Snap, _SQN}) -> ok = ink_close(Snap) end, Snapshots). + lists:foreach(fun({Snap, _TS, _SQN}) -> ok = ink_close(Snap) end, + Snapshots). -spec shutdown_manifest(leveled_imanifest:manifest()) -> ok. %% @doc From 28d0aef5fec893deea752da895594627cb1a3b83 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Thu, 24 Jan 2019 15:46:17 +0000 Subject: [PATCH 04/15] Make check that compaction not ongoing before accepting new compaction Respond 'busy' if compaction is ongoing --- src/leveled_bookie.erl | 6 +++--- src/leveled_inker.erl | 23 ++++++++++++++--------- test/end_to_end/basic_SUITE.erl | 1 + 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/leveled_bookie.erl b/src/leveled_bookie.erl index 73c0c33..9e547b4 100644 --- a/src/leveled_bookie.erl +++ b/src/leveled_bookie.erl @@ -1004,7 +1004,7 @@ book_snapshot(Pid, SnapType, Query, LongRunning) -> gen_server:call(Pid, {snapshot, SnapType, Query, LongRunning}, infinity). --spec book_compactjournal(pid(), integer()) -> ok. +-spec book_compactjournal(pid(), integer()) -> ok|busy. -spec book_islastcompactionpending(pid()) -> boolean(). -spec book_trimjournal(pid()) -> ok. @@ -1371,10 +1371,10 @@ handle_call({return_runner, QueryType}, _From, State) -> fold_countdown = CountDown}}; handle_call({compact_journal, Timeout}, _From, State) when State#state.head_only == false -> - ok = leveled_inker:ink_compactjournal(State#state.inker, + R = leveled_inker:ink_compactjournal(State#state.inker, self(), Timeout), - {reply, ok, State}; + {reply, R, State}; handle_call(confirm_compact, _From, State) when State#state.head_only == false -> {reply, leveled_inker:ink_compactionpending(State#state.inker), State}; diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index d2ad02b..dd6b86d 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -348,7 +348,7 @@ ink_loadpcl(Pid, MinSQN, FilterFun, Penciller) -> as_ink}, infinity). --spec ink_compactjournal(pid(), pid(), integer()) -> ok. +-spec ink_compactjournal(pid(), pid(), integer()) -> ok|busy. %% @doc %% Trigger a compaction event. the compaction event will use a sqn check %% against the Ledger to see if a value can be compacted - if the penciller @@ -605,14 +605,19 @@ handle_call({compact, FilterFun, Timeout}, _From, State) -> - leveled_iclerk:clerk_compact(State#state.clerk, - Checker, - InitiateFun, - CloseFun, - FilterFun, - self(), - Timeout), - {reply, ok, State#state{compaction_pending=true}}; + case State#state.compaction_pending of + true -> + {reply, busy, State}; + false -> + leveled_iclerk:clerk_compact(State#state.clerk, + Checker, + InitiateFun, + CloseFun, + FilterFun, + self(), + Timeout), + {reply, ok, State#state{compaction_pending=true}} + end; handle_call(compaction_complete, _From, State) -> {reply, ok, State#state{compaction_pending=false}}; handle_call(compaction_pending, _From, State) -> diff --git a/test/end_to_end/basic_SUITE.erl b/test/end_to_end/basic_SUITE.erl index 5aef2e8..a17108a 100644 --- a/test/end_to_end/basic_SUITE.erl +++ b/test/end_to_end/basic_SUITE.erl @@ -300,6 +300,7 @@ journal_compaction_tester(Restart, WRP) -> {sync_strategy, testutil:sync_strategy()}], {ok, Bookie3} = leveled_bookie:book_start(StartOpts2), ok = leveled_bookie:book_compactjournal(Bookie3, 30000), + busy = leveled_bookie:book_compactjournal(Bookie3, 30000), testutil:wait_for_compaction(Bookie3), ok = leveled_bookie:book_close(Bookie3), From 0333604fd93278e06e35ed4d9e2d699e010e59b8 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Thu, 24 Jan 2019 21:32:54 +0000 Subject: [PATCH 05/15] Change to cast in inker/iclerk interaction This allows for leveled_iclerk:clerk_stop to be a sync call, so that files will only be closed once the iclerk has stopped. This is designed ot prevent iclerk crashes during shutdowns when files it is depnding on are closed mid shutdown. --- src/leveled_iclerk.erl | 81 +++++++++++++-------------- src/leveled_inker.erl | 94 +++++++++++++------------------- test/end_to_end/tictac_SUITE.erl | 3 + 3 files changed, 78 insertions(+), 100 deletions(-) diff --git a/src/leveled_iclerk.erl b/src/leveled_iclerk.erl index 5f6b09a..cd8c547 100644 --- a/src/leveled_iclerk.erl +++ b/src/leveled_iclerk.erl @@ -82,9 +82,10 @@ code_change/3]). -export([clerk_new/1, - clerk_compact/7, + clerk_compact/6, clerk_hashtablecalc/3, clerk_trim/3, + clerk_promptdeletions/3, clerk_stop/1, clerk_loglevel/2, clerk_addlogs/2, @@ -148,25 +149,30 @@ clerk_new(InkerClerkOpts) -> -spec clerk_compact(pid(), pid(), fun(), fun(), fun(), - pid(), integer()) -> ok. + list()) -> ok. %% @doc %% Trigger a compaction for this clerk if the threshold of data recovery has %% been met -clerk_compact(Pid, Checker, InitiateFun, CloseFun, FilterFun, Inker, TimeO) -> +clerk_compact(Pid, Checker, InitiateFun, CloseFun, FilterFun, Manifest) -> gen_server:cast(Pid, {compact, Checker, InitiateFun, CloseFun, FilterFun, - Inker, - TimeO}). + Manifest}). --spec clerk_trim(pid(), pid(), integer()) -> ok. +-spec clerk_trim(pid(), integer(), list()) -> ok. %% @doc %% Trim the Inker back to the persisted SQN -clerk_trim(Pid, Inker, PersistedSQN) -> - gen_server:cast(Pid, {trim, Inker, PersistedSQN}). +clerk_trim(Pid, PersistedSQN, ManifestAsList) -> + gen_server:cast(Pid, {trim, PersistedSQN, ManifestAsList}). + +-spec clerk_promptdeletions(pid(), pos_integer(), list()) -> ok. +%% @doc +%% +clerk_promptdeletions(Pid, ManifestSQN, DeletedFiles) -> + gen_server:cast(Pid, {prompt_deletions, ManifestSQN, DeletedFiles}). -spec clerk_hashtablecalc(ets:tid(), integer(), pid()) -> ok. %% @doc @@ -182,7 +188,7 @@ clerk_hashtablecalc(HashTree, StartPos, CDBpid) -> %% @doc %% Stop the clerk clerk_stop(Pid) -> - gen_server:cast(Pid, stop). + gen_server:call(Pid, stop, 60000). -spec clerk_loglevel(pid(), leveled_log:log_level()) -> ok. %% @doc @@ -247,10 +253,10 @@ init([LogOpts, IClerkOpts]) -> compression_method = IClerkOpts#iclerk_options.compression_method}}. -handle_call(_Msg, _From, State) -> - {reply, not_supported, State}. +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. -handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Inker, _TO}, +handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Manifest0}, State) -> % Empty the waste folder clear_waste(State), @@ -260,7 +266,8 @@ handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Inker, _TO}, % Need to fetch manifest at start rather than have it be passed in % Don't want to process a queued call waiting on an old manifest - [_Active|Manifest] = leveled_inker:ink_getmanifest(Inker), + [_Active|Manifest] = Manifest0, + Inker = State#state.inker, MaxRunLength = State#state.max_run_length, {FilterServer, MaxSQN} = InitiateFun(Checker), CDBopts = State#state.cdb_options, @@ -291,24 +298,29 @@ handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Inker, _TO}, end, BestRun1), leveled_log:log("IC002", [length(FilesToDelete)]), - case is_process_alive(Inker) of - true -> - update_inker(Inker, - ManifestSlice, - FilesToDelete), - ok = CloseFun(FilterServer), - {noreply, State} - end; + ok = leveled_inker:ink_clerkcomplete(Inker, + ManifestSlice, + FilesToDelete), + ok = CloseFun(FilterServer), + {noreply, State}; false -> - ok = leveled_inker:ink_compactioncomplete(Inker), + ok = leveled_inker:ink_clerkcomplete(Inker, [], []), ok = CloseFun(FilterServer), {noreply, State} end; -handle_cast({trim, Inker, PersistedSQN}, State) -> - ManifestAsList = leveled_inker:ink_getmanifest(Inker), +handle_cast({trim, PersistedSQN, ManifestAsList}, State) -> FilesToDelete = leveled_imanifest:find_persistedentries(PersistedSQN, ManifestAsList), - ok = update_inker(Inker, [], FilesToDelete), + leveled_log:log("IC007", []), + ok = leveled_inker:ink_clerkcomplete(State#state.inker, [], FilesToDelete), + {noreply, State}; +handle_cast({prompt_deletions, ManifestSQN, FilesToDelete}, State) -> + lists:foreach(fun({_SQN, _FN, J2D, _LK}) -> + leveled_cdb:cdb_deletepending(J2D, + ManifestSQN, + State#state.inker) + end, + FilesToDelete), {noreply, State}; handle_cast({hashtable_calc, HashTree, StartPos, CDBpid}, State) -> {IndexList, HashTreeBin} = leveled_cdb:hashtable_calc(HashTree, StartPos), @@ -328,9 +340,7 @@ handle_cast({remove_logs, ForcedLogs}, State) -> ok = leveled_log:remove_forcedlogs(ForcedLogs), CDBopts = State#state.cdb_options, CDBopts0 = CDBopts#cdb_options{log_options = leveled_log:get_opts()}, - {noreply, State#state{cdb_options = CDBopts0}}; -handle_cast(stop, State) -> - {stop, normal, State}. + {noreply, State#state{cdb_options = CDBopts0}}. handle_info(_Info, State) -> {noreply, State}. @@ -613,20 +623,6 @@ sort_run(RunOfFiles) -> Cand1#candidate.low_sqn =< Cand2#candidate.low_sqn end, lists:sort(CompareFun, RunOfFiles). -update_inker(Inker, ManifestSlice, FilesToDelete) -> - {ok, ManSQN} = leveled_inker:ink_updatemanifest(Inker, - ManifestSlice, - FilesToDelete), - ok = leveled_inker:ink_compactioncomplete(Inker), - leveled_log:log("IC007", []), - lists:foreach(fun({_SQN, _FN, J2D, _LK}) -> - leveled_cdb:cdb_deletepending(J2D, - ManSQN, - Inker) - end, - FilesToDelete), - ok. - compact_files(BestRun, CDBopts, FilterFun, FilterServer, MaxSQN, RStrategy, PressMethod) -> BatchesOfPositions = get_all_positions(BestRun, []), @@ -1147,7 +1143,6 @@ size_score_test() -> coverage_cheat_test() -> {noreply, _State0} = handle_info(timeout, #state{}), {ok, _State1} = code_change(null, #state{}, null), - {reply, not_supported, _State2} = handle_call(null, null, #state{}), terminate(error, #state{}). -endif. diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index dd6b86d..d25f6d9 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -105,11 +105,10 @@ ink_registersnapshot/2, ink_confirmdelete/2, ink_compactjournal/3, - ink_compactioncomplete/1, + ink_clerkcomplete/3, ink_compactionpending/1, ink_trim/2, ink_getmanifest/1, - ink_updatemanifest/3, ink_printmanifest/1, ink_close/1, ink_doom/1, @@ -359,7 +358,7 @@ ink_loadpcl(Pid, MinSQN, FilterFun, Penciller) -> %% that any value that was written more recently than the last flush to disk %% of the Ledger will not be considered for compaction (as this may be %% required to reload the Ledger on startup). -ink_compactjournal(Pid, Bookie, Timeout) -> +ink_compactjournal(Pid, Bookie, _Timeout) -> CheckerInitiateFun = fun initiate_penciller_snapshot/1, CheckerCloseFun = fun leveled_penciller:pcl_close/1, CheckerFilterFun = @@ -369,28 +368,26 @@ ink_compactjournal(Pid, Bookie, Timeout) -> Bookie, CheckerInitiateFun, CheckerCloseFun, - CheckerFilterFun, - Timeout}, + CheckerFilterFun}, infinity). %% Allows the Checker to be overriden in test, use something other than a %% penciller -ink_compactjournal(Pid, Checker, InitiateFun, CloseFun, FilterFun, Timeout) -> +ink_compactjournal(Pid, Checker, InitiateFun, CloseFun, FilterFun, _Timeout) -> gen_server:call(Pid, {compact, Checker, InitiateFun, CloseFun, - FilterFun, - Timeout}, + FilterFun}, infinity). --spec ink_compactioncomplete(pid()) -> ok. +-spec ink_clerkcomplete(pid(), list(), list()) -> ok. %% @doc %% Used by a clerk to state that a compaction process is over, only change %% is to unlock the Inker for further compactions. -ink_compactioncomplete(Pid) -> - gen_server:call(Pid, compaction_complete, infinity). +ink_clerkcomplete(Pid, ManifestSnippet, FilesToDelete) -> + gen_server:cast(Pid, {clerk_complete, ManifestSnippet, FilesToDelete}). -spec ink_compactionpending(pid()) -> boolean(). %% @doc @@ -425,21 +422,6 @@ ink_backup(Pid, BackupPath) -> ink_getmanifest(Pid) -> gen_server:call(Pid, get_manifest, infinity). --spec ink_updatemanifest(pid(), list(), list()) -> {ok, integer()}. -%% @doc -%% Add a section of new entries into the manifest, and drop a bunch of deleted -%% files out of the manifest. Used to update the manifest after a compaction -%% job. -%% -%% Returns {ok, ManSQN} with the ManSQN being the sequence number of the -%% updated manifest -ink_updatemanifest(Pid, ManifestSnippet, DeletedFiles) -> - gen_server:call(Pid, - {update_manifest, - ManifestSnippet, - DeletedFiles}, - infinity). - -spec ink_printmanifest(pid()) -> ok. %% @doc %% Used in tests to print out the manifest @@ -574,27 +556,6 @@ handle_call({confirm_delete, ManSQN}, _From, State) -> State#state{registered_snapshots = RegisteredSnapshots0}}; handle_call(get_manifest, _From, State) -> {reply, leveled_imanifest:to_list(State#state.manifest), State}; -handle_call({update_manifest, - ManifestSnippet, - DeletedFiles}, _From, State) -> - DropFun = - fun(E, Acc) -> - leveled_imanifest:remove_entry(Acc, E) - end, - Man0 = lists:foldl(DropFun, State#state.manifest, DeletedFiles), - AddFun = - fun(E, Acc) -> - leveled_imanifest:add_entry(Acc, E, false) - end, - Man1 = lists:foldl(AddFun, Man0, ManifestSnippet), - NewManifestSQN = State#state.manifest_sqn + 1, - leveled_imanifest:printer(Man1), - leveled_imanifest:writer(Man1, NewManifestSQN, State#state.root_path), - {reply, - {ok, NewManifestSQN}, - State#state{manifest=Man1, - manifest_sqn=NewManifestSQN, - pending_removals=DeletedFiles}}; handle_call(print_manifest, _From, State) -> leveled_imanifest:printer(State#state.manifest), {reply, ok, State}; @@ -602,28 +563,26 @@ handle_call({compact, Checker, InitiateFun, CloseFun, - FilterFun, - Timeout}, + FilterFun}, _From, State) -> case State#state.compaction_pending of true -> {reply, busy, State}; false -> + Manifest = leveled_imanifest:to_list(State#state.manifest), leveled_iclerk:clerk_compact(State#state.clerk, Checker, InitiateFun, CloseFun, FilterFun, - self(), - Timeout), + Manifest), {reply, ok, State#state{compaction_pending=true}} end; -handle_call(compaction_complete, _From, State) -> - {reply, ok, State#state{compaction_pending=false}}; handle_call(compaction_pending, _From, State) -> {reply, State#state.compaction_pending, State}; handle_call({trim, PersistedSQN}, _From, State) -> - ok = leveled_iclerk:clerk_trim(State#state.clerk, self(), PersistedSQN), + Manifest = leveled_imanifest:to_list(State#state.manifest), + ok = leveled_iclerk:clerk_trim(State#state.clerk, PersistedSQN, Manifest), {reply, ok, State}; handle_call(roll, _From, State) -> case leveled_cdb:cdb_lastkey(State#state.active_journaldb) of @@ -717,7 +676,7 @@ handle_call(close, _From, State) -> leveled_log:log("I0005", [close]), leveled_log:log("I0006", [State#state.journal_sqn, State#state.manifest_sqn]), - leveled_iclerk:clerk_stop(State#state.clerk), + ok = leveled_iclerk:clerk_stop(State#state.clerk), shutdown_snapshots(State#state.registered_snapshots), shutdown_manifest(State#state.manifest) end, @@ -732,12 +691,33 @@ handle_call(doom, _From, State) -> leveled_log:log("I0005", [doom]), leveled_log:log("I0006", [State#state.journal_sqn, State#state.manifest_sqn]), - leveled_iclerk:clerk_stop(State#state.clerk), + ok = leveled_iclerk:clerk_stop(State#state.clerk), shutdown_snapshots(State#state.registered_snapshots), shutdown_manifest(State#state.manifest), - {stop, normal, {ok, FPs}, State}. + +handle_cast({clerk_complete, ManifestSnippet, FilesToDelete}, State) -> + DropFun = + fun(E, Acc) -> + leveled_imanifest:remove_entry(Acc, E) + end, + Man0 = lists:foldl(DropFun, State#state.manifest, FilesToDelete), + AddFun = + fun(E, Acc) -> + leveled_imanifest:add_entry(Acc, E, false) + end, + Man1 = lists:foldl(AddFun, Man0, ManifestSnippet), + NewManifestSQN = State#state.manifest_sqn + 1, + leveled_imanifest:printer(Man1), + leveled_imanifest:writer(Man1, NewManifestSQN, State#state.root_path), + ok = leveled_iclerk:clerk_promptdeletions(State#state.clerk, + NewManifestSQN, + FilesToDelete), + {noreply, State#state{manifest=Man1, + manifest_sqn=NewManifestSQN, + pending_removals=FilesToDelete, + compaction_pending=false}}; handle_cast({release_snapshot, Snapshot}, State) -> Rs = lists:keydelete(Snapshot, 1, State#state.registered_snapshots), leveled_log:log("I0003", [Snapshot]), diff --git a/test/end_to_end/tictac_SUITE.erl b/test/end_to_end/tictac_SUITE.erl index e0f5f01..ba422b0 100644 --- a/test/end_to_end/tictac_SUITE.erl +++ b/test/end_to_end/tictac_SUITE.erl @@ -724,6 +724,9 @@ basic_headonly_test(ObjectCount, RemoveCount, HeadOnly) -> {ok, FinalFNs} = file:list_dir(JFP), + ok = leveled_bookie:book_trimjournal(Bookie1), + % CCheck a second trim is still OK + [{add, SegmentID0, Bucket0, Key0, Hash0}|_Rest] = ObjectSpecL, case HeadOnly of with_lookup -> From 7801f16de9efb6b8fee64531124e905d15cfa4eb Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 25 Jan 2019 10:24:47 +0000 Subject: [PATCH 06/15] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b266ee3..ae9a11c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ test/test_area/* cover cover_* .eqc-info +leveled_data/* From 5fab9e2d62d54d0723787c9ff397793c0c8ad4e2 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 25 Jan 2019 10:25:55 +0000 Subject: [PATCH 07/15] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8031a86..01c0c33 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .DS_Store rebar.lock test/test_area/* +leveled_data/* From e349774167831a369229bc55a561169a2fcb75a3 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 25 Jan 2019 12:11:42 +0000 Subject: [PATCH 08/15] Allow clerk to be stopped during compaction scoring This will stop needless compaction work from being completed when the iclerk is sent a close at this stage. --- src/leveled_iclerk.erl | 101 ++++++++++++++++++++--------- test/end_to_end/recovery_SUITE.erl | 24 ++++++- 2 files changed, 91 insertions(+), 34 deletions(-) diff --git a/src/leveled_iclerk.erl b/src/leveled_iclerk.erl index cd8c547..0e2b2ae 100644 --- a/src/leveled_iclerk.erl +++ b/src/leveled_iclerk.erl @@ -115,15 +115,24 @@ reload_strategy = ?DEFAULT_RELOAD_STRATEGY :: list(), singlefile_compactionperc = ?SINGLEFILE_COMPACTION_TARGET :: float(), maxrunlength_compactionperc = ?MAXRUNLENGTH_COMPACTION_TARGET ::float(), - compression_method = native :: lz4|native}). + compression_method = native :: lz4|native, + scored_files = [] :: list(candidate()), + scoring_state :: scoring_state()|undefined}). -record(candidate, {low_sqn :: integer() | undefined, filename :: string() | undefined, journal :: pid() | undefined, compaction_perc :: float() | undefined}). +-record(scoring_state, {filter_fun :: fun(), + filter_server :: pid(), + max_sqn :: non_neg_integer(), + close_fun :: fun(), + start_time :: erlang:timestamp()}). + -type iclerk_options() :: #iclerk_options{}. -type candidate() :: #candidate{}. +-type scoring_state() :: #scoring_state{}. -type score_parameters() :: {integer(), float(), float()}. % Score parameters are a tuple % - of maximum run length; how long a run of consecutive files can be for @@ -188,7 +197,7 @@ clerk_hashtablecalc(HashTree, StartPos, CDBpid) -> %% @doc %% Stop the clerk clerk_stop(Pid) -> - gen_server:call(Pid, stop, 60000). + gen_server:call(Pid, stop, infinity). -spec clerk_loglevel(pid(), leveled_log:log_level()) -> ok. %% @doc @@ -208,6 +217,16 @@ clerk_addlogs(Pid, ForcedLogs) -> clerk_removelogs(Pid, ForcedLogs) -> gen_server:cast(Pid, {remove_logs, ForcedLogs}). + +-spec clerk_scorefilelist(pid(), list(candidate())) -> ok. +%% @doc +%% Score the file at the head of the list and then send the tail of the list to +%% be scored +clerk_scorefilelist(Pid, []) -> + gen_server:cast(Pid, scoring_complete); +clerk_scorefilelist(Pid, CandidateList) -> + gen_server:cast(Pid, {score_filelist, CandidateList}). + %%%============================================================================ %%% gen_server callbacks %%%============================================================================ @@ -254,6 +273,16 @@ init([LogOpts, IClerkOpts]) -> IClerkOpts#iclerk_options.compression_method}}. handle_call(stop, _From, State) -> + case State#state.scoring_state of + undefined -> + ok; + ScoringState -> + % Closed when scoring files, and so need to shutdown FilterServer + % to close down neatly + CloseFun = ScoringState#scoring_state.close_fun, + FilterServer = ScoringState#scoring_state.filter_server, + CloseFun(FilterServer) + end, {stop, normal, ok, State}. handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Manifest0}, @@ -267,12 +296,42 @@ handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Manifest0}, % Need to fetch manifest at start rather than have it be passed in % Don't want to process a queued call waiting on an old manifest [_Active|Manifest] = Manifest0, - Inker = State#state.inker, - MaxRunLength = State#state.max_run_length, {FilterServer, MaxSQN} = InitiateFun(Checker), + ok = clerk_scorefilelist(self(), Manifest), + ScoringState = + #scoring_state{filter_fun = FilterFun, + filter_server = FilterServer, + max_sqn = MaxSQN, + close_fun = CloseFun, + start_time = SW}, + {noreply, State#state{scored_files = [], scoring_state = ScoringState}}; +handle_cast({score_filelist, [Entry|Tail]}, State) -> + Candidates = State#state.scored_files, + {LowSQN, FN, JournalP, _LK} = Entry, + ScoringState = State#state.scoring_state, + CpctPerc = check_single_file(JournalP, + ScoringState#scoring_state.filter_fun, + ScoringState#scoring_state.filter_server, + ScoringState#scoring_state.max_sqn, + ?SAMPLE_SIZE, + ?BATCH_SIZE), + Candidate = + #candidate{low_sqn = LowSQN, + filename = FN, + journal = JournalP, + compaction_perc = CpctPerc}, + ok = clerk_scorefilelist(self(), Tail), + {noreply, State#state{scored_files = [Candidate|Candidates]}}; +handle_cast(scoring_complete, State) -> + MaxRunLength = State#state.max_run_length, CDBopts = State#state.cdb_options, - - Candidates = scan_all_files(Manifest, FilterFun, FilterServer, MaxSQN), + Candidates = lists:reverse(State#state.scored_files), + ScoringState = State#state.scoring_state, + FilterFun = ScoringState#scoring_state.filter_fun, + FilterServer = ScoringState#scoring_state.filter_server, + MaxSQN = ScoringState#scoring_state.max_sqn, + CloseFun = ScoringState#scoring_state.close_fun, + SW = ScoringState#scoring_state.start_time, ScoreParams = {MaxRunLength, State#state.maxrunlength_compactionperc, @@ -298,15 +357,15 @@ handle_cast({compact, Checker, InitiateFun, CloseFun, FilterFun, Manifest0}, end, BestRun1), leveled_log:log("IC002", [length(FilesToDelete)]), - ok = leveled_inker:ink_clerkcomplete(Inker, + ok = leveled_inker:ink_clerkcomplete(State#state.inker, ManifestSlice, FilesToDelete), ok = CloseFun(FilterServer), - {noreply, State}; + {noreply, State#state{scoring_state = undefined}}; false -> - ok = leveled_inker:ink_clerkcomplete(Inker, [], []), + ok = leveled_inker:ink_clerkcomplete(State#state.inker, [], []), ok = CloseFun(FilterServer), - {noreply, State} + {noreply, State#state{scoring_state = undefined}} end; handle_cast({trim, PersistedSQN, ManifestAsList}, State) -> FilesToDelete = @@ -487,28 +546,6 @@ size_comparison_score(KeySizeList, FilterFun, FilterServer, MaxSQN) -> 100 * ActiveSize / (ActiveSize + ReplacedSize) end. -scan_all_files(Manifest, FilterFun, FilterServer, MaxSQN) -> - scan_all_files(Manifest, FilterFun, FilterServer, MaxSQN, []). - -scan_all_files([], _FilterFun, _FilterServer, _MaxSQN, CandidateList) -> - CandidateList; -scan_all_files([Entry|Tail], FilterFun, FilterServer, MaxSQN, CandidateList) -> - {LowSQN, FN, JournalP, _LK} = Entry, - CpctPerc = check_single_file(JournalP, - FilterFun, - FilterServer, - MaxSQN, - ?SAMPLE_SIZE, - ?BATCH_SIZE), - scan_all_files(Tail, - FilterFun, - FilterServer, - MaxSQN, - CandidateList ++ - [#candidate{low_sqn = LowSQN, - filename = FN, - journal = JournalP, - compaction_perc = CpctPerc}]). fetch_inbatches([], _BatchSize, _CDB, CheckedList) -> CheckedList; diff --git a/test/end_to_end/recovery_SUITE.erl b/test/end_to_end/recovery_SUITE.erl index 304b1d7..fb4a9db 100644 --- a/test/end_to_end/recovery_SUITE.erl +++ b/test/end_to_end/recovery_SUITE.erl @@ -10,7 +10,8 @@ recovr_strategy/1, aae_missingjournal/1, aae_bustedjournal/1, - journal_compaction_bustedjournal/1 + journal_compaction_bustedjournal/1, + close_duringcompaction/1 ]). all() -> [ @@ -21,10 +22,29 @@ all() -> [ recovr_strategy, aae_missingjournal, aae_bustedjournal, - journal_compaction_bustedjournal + journal_compaction_bustedjournal, + close_duringcompaction ]. +close_duringcompaction(_Config) -> + % Prompt a compaction, and close immedately - confirm that the close + % happens without error. + % This should trigger the iclerk to receive a close during the file + % scoring stage + RootPath = testutil:reset_filestructure(), + BookOpts = [{root_path, RootPath}, + {cache_size, 2000}, + {max_journalsize, 2000000}, + {sync_strategy, testutil:sync_strategy()}], + {ok, Spcl1, LastV1} = rotating_object_check(BookOpts, "Bucket1", 6400), + {ok, Book1} = leveled_bookie:book_start(BookOpts), + ok = leveled_bookie:book_compactjournal(Book1, 30000), + ok = leveled_bookie:book_close(Book1), + {ok, Book2} = leveled_bookie:book_start(BookOpts), + ok = testutil:check_indexed_objects(Book2, "Bucket1", Spcl1, LastV1), + ok = leveled_bookie:book_close(Book2). + recovery_with_samekeyupdates(_Config) -> % Setup to prove https://github.com/martinsumner/leveled/issues/229 % run a test that involves many updates to the same key, and check that From 5b54affbf0ee9b1e0f3bb488d315ff1221a275d4 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 25 Jan 2019 14:32:41 +0000 Subject: [PATCH 09/15] Have inker reopen compacted files The inker cler will now close compacted files before prompting the inker to update the manifest. The inker should reopen those files, so that the file processes are linked to it and not the clerk. This also stops a stopped clerk leading to orphaned cdb files. --- src/leveled_iclerk.erl | 28 +++++++++++++++++----------- src/leveled_imanifest.erl | 3 +++ src/leveled_inker.erl | 10 ++++++++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/leveled_iclerk.erl b/src/leveled_iclerk.erl index 0e2b2ae..076d73b 100644 --- a/src/leveled_iclerk.erl +++ b/src/leveled_iclerk.erl @@ -227,6 +227,7 @@ clerk_scorefilelist(Pid, []) -> clerk_scorefilelist(Pid, CandidateList) -> gen_server:cast(Pid, {score_filelist, CandidateList}). + %%%============================================================================ %%% gen_server callbacks %%%============================================================================ @@ -357,14 +358,14 @@ handle_cast(scoring_complete, State) -> end, BestRun1), leveled_log:log("IC002", [length(FilesToDelete)]), + ok = CloseFun(FilterServer), ok = leveled_inker:ink_clerkcomplete(State#state.inker, ManifestSlice, FilesToDelete), - ok = CloseFun(FilterServer), {noreply, State#state{scoring_state = undefined}}; false -> - ok = leveled_inker:ink_clerkcomplete(State#state.inker, [], []), ok = CloseFun(FilterServer), + ok = leveled_inker:ink_clerkcomplete(State#state.inker, [], []), {noreply, State#state{scoring_state = undefined}} end; handle_cast({trim, PersistedSQN, ManifestAsList}, State) -> @@ -794,8 +795,7 @@ write_values(KVCList, CDBopts, Journal0, ManSlice0, PressMethod) -> SQN, compact_journal), leveled_log:log("IC009", [FN]), - leveled_cdb:cdb_open_writer(FN, - CDBopts); + leveled_cdb:cdb_open_writer(FN, CDBopts); _ -> {ok, Journal0} end, @@ -1018,9 +1018,10 @@ compact_single_file_recovr_test() -> LedgerFun1, CompactFP, CDB} = compact_single_file_setup(), - [{LowSQN, FN, PidR, _LastKey}] = + CDBOpts = #cdb_options{binary_mode=true}, + [{LowSQN, FN, _PidOldR, LastKey}] = compact_files([Candidate], - #cdb_options{file_path=CompactFP, binary_mode=true}, + CDBOpts#cdb_options{file_path=CompactFP}, LedgerFun1, LedgerSrv1, 9, @@ -1028,6 +1029,7 @@ compact_single_file_recovr_test() -> native), io:format("FN of ~s~n", [FN]), ?assertMatch(2, LowSQN), + {ok, PidR} = leveled_cdb:cdb_reopen_reader(FN, LastKey, CDBOpts), ?assertMatch(probably, leveled_cdb:cdb_keycheck(PidR, {8, @@ -1047,6 +1049,7 @@ compact_single_file_recovr_test() -> test_ledgerkey("Key2")}), ?assertMatch({{_, _}, {"Value2", {[], infinity}}}, leveled_codec:from_inkerkv(RKV1)), + ok = leveled_cdb:cdb_close(PidR), ok = leveled_cdb:cdb_deletepending(CDB), ok = leveled_cdb:cdb_destroy(CDB). @@ -1057,9 +1060,10 @@ compact_single_file_retain_test() -> LedgerFun1, CompactFP, CDB} = compact_single_file_setup(), - [{LowSQN, FN, PidR, _LK}] = + CDBOpts = #cdb_options{binary_mode=true}, + [{LowSQN, FN, _PidOldR, LastKey}] = compact_files([Candidate], - #cdb_options{file_path=CompactFP, binary_mode=true}, + CDBOpts#cdb_options{file_path=CompactFP}, LedgerFun1, LedgerSrv1, 9, @@ -1067,6 +1071,7 @@ compact_single_file_retain_test() -> native), io:format("FN of ~s~n", [FN]), ?assertMatch(1, LowSQN), + {ok, PidR} = leveled_cdb:cdb_reopen_reader(FN, LastKey, CDBOpts), ?assertMatch(probably, leveled_cdb:cdb_keycheck(PidR, {8, @@ -1081,11 +1086,12 @@ compact_single_file_retain_test() -> stnd, test_ledgerkey("Key1")})), RKV1 = leveled_cdb:cdb_get(PidR, - {2, - stnd, - test_ledgerkey("Key2")}), + {2, + stnd, + test_ledgerkey("Key2")}), ?assertMatch({{_, _}, {"Value2", {[], infinity}}}, leveled_codec:from_inkerkv(RKV1)), + ok = leveled_cdb:cdb_close(PidR), ok = leveled_cdb:cdb_deletepending(CDB), ok = leveled_cdb:cdb_destroy(CDB). diff --git a/src/leveled_imanifest.erl b/src/leveled_imanifest.erl index fb72e84..24da2b8 100644 --- a/src/leveled_imanifest.erl +++ b/src/leveled_imanifest.erl @@ -52,6 +52,9 @@ generate_entry(Journal) -> case leveled_cdb:cdb_firstkey(PidR) of {StartSQN, _Type, _PK} -> LastKey = leveled_cdb:cdb_lastkey(PidR), + % close the file here. This will then be re-opened by the inker + % and so will be correctly linked to the inker not to the iclerk + ok = leveled_cdb:cdb_close(PidR), [{StartSQN, NewFN, PidR, LastKey}]; empty -> leveled_log:log("IC013", [NewFN]), diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index d25f6d9..22266e4 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -698,14 +698,20 @@ handle_call(doom, _From, State) -> handle_cast({clerk_complete, ManifestSnippet, FilesToDelete}, State) -> + CDBOpts = State#state.cdb_options, DropFun = fun(E, Acc) -> leveled_imanifest:remove_entry(Acc, E) end, Man0 = lists:foldl(DropFun, State#state.manifest, FilesToDelete), AddFun = - fun(E, Acc) -> - leveled_imanifest:add_entry(Acc, E, false) + fun(ManEntry, Acc) -> + {LowSQN, FN, _, LK_RO} = ManEntry, + % At this stage the FN has a .cdb extension, which will be + % stripped during add_entry - so need to add the .cdb here + {ok, Pid} = leveled_cdb:cdb_reopen_reader(FN, LK_RO, CDBOpts), + UpdEntry = {LowSQN, FN, Pid, LK_RO}, + leveled_imanifest:add_entry(Acc, UpdEntry, false) end, Man1 = lists:foldl(AddFun, Man0, ManifestSnippet), NewManifestSQN = State#state.manifest_sqn + 1, From f7022627e5ed75fad69255b88acf7016312239d3 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 25 Jan 2019 19:11:34 +0000 Subject: [PATCH 10/15] Check not pending before compacting Also check for existence before deleting a CDB file --- .eqc-info | Bin 8945 -> 57751 bytes current_counterexample.eqc | Bin 217582 -> 1680 bytes src/leveled_bookie.erl | 22 +++++++---- src/leveled_cdb.erl | 24 +++++++----- src/leveled_inker.erl | 27 +++++-------- src/leveled_log.erl | 7 +++- test/leveledjc_eqc.erl | 76 ++++++++++++------------------------- 7 files changed, 68 insertions(+), 88 deletions(-) diff --git a/.eqc-info b/.eqc-info index 7dba85ac8475d61c59a9871e02448ff8a3ad8622..663432b9ecb73c15a9bfb20d9633269a6fbdde2e 100644 GIT binary patch literal 57751 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 delta 685 zcmbP!nE9hC3j>2<;zpJ@UN#m@1_oJ+&5o?U*(N88eC6}Mn8d(fbPq^`1SSe?c4WN5 z$i;YodG*=%3=E7rCcB6?vNPuAFfcGMO}-#*H0glkWOpf<$p+H$lM`0531n6?ikvfB z{r2LCN7EZ?4*O32CnYxdgPH7P`Mt6r1tRDQQWzNXCud1#EVhzl;bPGgG6}0-U|>-M z+M~+IHaTZ%DkC${9<#~92PN2&^K%OllP4P;e zXmy+n&}rg8%#gyso0D3WnvChs?6({7wqdBJ>tsKtGTDR(C9`L#0_sB*O_BaqB&26>z#gDHi9H904- zC~>l{jZ_Ahn^~Ngn4HK0VzVWs7L{bCCbA-P*ubh%b4v2_64}9QumOo2SwIs(W-BsI zjaXRkdiCAiSzJNi?r5!LwcMMiv8a{zquk_u_x1&FT$)^HD?8bHhT!C5Gh~2{RjJ43 z(D`78#)2IRu?ONZMzCYjQ}c_`Qxlnx9KZ}>v!oTJra)ZJnvqykR0(xFR6W@7Y>7D_ zk0r9hT&$$X*z~pi^IF!5{w3ultQ_;LHE%BtbN9V=CH;GqseG%I#wNIv8pIMVWS pQsRqB^T4Sm5o|GY888%?va&!z5GNL;7MJFfY-V{dk&#L1BLIMj-h==E diff --git a/current_counterexample.eqc b/current_counterexample.eqc index c4c9f59c315444d99fded54af30aa076bddbfa3e..aafab95ece01758cee7f887e999924a2b1ca84bd 100644 GIT binary patch literal 1680 zcmZq9VPIfj%wS4kU@lHA1aUY(9M;_Yl+>IQ2Hu?1vecZ^l&s|V)WYNpX0Wmnu=29R zqD00FmJ|k-sANez%BKS(kwzqBYXF$W}?#K7RE0Fn?&&d)68a#0viu@bxnjgJR?h)Lkx7GS^o!5zjpG>!3 z@0>fMG2g~+*2A|GEVtfm-mbG+^39}{+kVT>e{`GiW1h^Bqy;YbY@L-3oUaLazu9T0 z*RrPF<}Px|mcO}HJQlMyJ$^QwPyBI?Lwi$jLmSKfdyeXbEGavuKfdc!##&aX?vy;I zyK;5>0fE(yhxZ?AJ8-Hq*Q%nucU#Um#ZD_l@AJo(Jz9BuVsY$S_Hg-%y|c5^kM}oQ zpG}w))6CQ=cxmT{YoI{zH)R9{jBYbHxqu>yH77MQClQ#=fNZv;)S{Bi)I=sQCp|U4 zC_OciISZ%(sELu0aij+zFd+DmlNDns128Q^vRoEsa%RSpoLRDg*$EU0kdgtMkTFeQ z!DGU(%l)i)Jj+sCl3#!tTr8Nu#fHa(VHaHNcs$DvDt!}^OF$t8&dlJFk}Z)TD=P~C D{RLI* literal 217582 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 -spec book_compactjournal(pid(), integer()) -> ok|busy. --spec book_eqccompactjournal(pid(), integer()) -> {ok, pid()}. +-spec book_eqccompactjournal(pid(), integer()) -> {ok|busy, pid()|undefined}. -spec book_islastcompactionpending(pid()) -> boolean(). -spec book_trimjournal(pid()) -> ok. @@ -1016,8 +1016,7 @@ book_snapshot(Pid, SnapType, Query, LongRunning) -> %% in Riak it will be triggered by a vnode callback. book_eqccompactjournal(Pid, Timeout) -> - {_R, P} = gen_server:call(Pid, {compact_journal, Timeout}, infinity), - {ok, P}. + gen_server:call(Pid, {compact_journal, Timeout}, infinity). book_compactjournal(Pid, Timeout) -> {R, _P} = gen_server:call(Pid, {compact_journal, Timeout}, infinity), @@ -1129,7 +1128,7 @@ init([Opts]) -> ConfiguredCacheSize = max(proplists:get_value(cache_size, Opts), ?MIN_CACHE_SIZE), CacheJitter = - ConfiguredCacheSize div (100 div ?CACHE_SIZE_JITTER), + max(1, ConfiguredCacheSize div (100 div ?CACHE_SIZE_JITTER)), CacheSize = ConfiguredCacheSize + erlang:phash2(self()) rem CacheJitter, PCLMaxSize = @@ -1378,10 +1377,17 @@ handle_call({return_runner, QueryType}, _From, State) -> fold_countdown = CountDown}}; handle_call({compact_journal, Timeout}, _From, State) when State#state.head_only == false -> - R = leveled_inker:ink_compactjournal(State#state.inker, - self(), - Timeout), - {reply, R, State}; + case leveled_inker:ink_compactionpending(State#state.inker) of + true -> + {reply, {busy, undefined}, State}; + false -> + {ok, PclSnap, null} = + snapshot_store(State, ledger, undefined, true), + R = leveled_inker:ink_compactjournal(State#state.inker, + PclSnap, + Timeout), + {reply, R, State} + end; handle_call(confirm_compact, _From, State) when State#state.head_only == false -> {reply, leveled_inker:ink_compactionpending(State#state.inker), State}; diff --git a/src/leveled_cdb.erl b/src/leveled_cdb.erl index 546945d..1693ed3 100644 --- a/src/leveled_cdb.erl +++ b/src/leveled_cdb.erl @@ -823,15 +823,21 @@ finished_rolling(CDB) -> %% If delete is pending - thent he close behaviour needs to actuallly delete %% the file close_pendingdelete(Handle, Filename, WasteFP) -> - case WasteFP of - undefined -> - ok = file:close(Handle), - ok = file:delete(Filename); - WasteFP -> - file:close(Handle), - Components = filename:split(Filename), - NewName = WasteFP ++ lists:last(Components), - file:rename(Filename, NewName) + ok = file:close(Handle), + case filelib:is_file(Filename) of + true -> + case WasteFP of + undefined -> + ok = file:delete(Filename); + WasteFP -> + Components = filename:split(Filename), + NewName = WasteFP ++ lists:last(Components), + file:rename(Filename, NewName) + end; + false -> + % This may happen when there has been a destroy while files are + % still pending deletion + leveled_log:log("CDB21", [Filename]) end. -spec set_writeops(sync|riak_sync|none) -> {list(), sync|riak_sync|none}. diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index 858a5e9..b27fbb7 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -276,7 +276,7 @@ ink_close(Pid) -> %% Test function used to close a file, and return all file paths (potentially %% to erase all persisted existence) ink_doom(Pid) -> - gen_server:call(Pid, doom, 60000). + gen_server:call(Pid, doom, infinity). -spec ink_fold(pid(), integer(), {fun(), fun(), fun()}, any()) -> fun(). %% @doc @@ -566,19 +566,14 @@ handle_call({compact, FilterFun}, _From, State) -> Clerk = State#state.clerk, - case State#state.compaction_pending of - true -> - {reply, {busy, Clerk}, State}; - false -> - Manifest = leveled_imanifest:to_list(State#state.manifest), - leveled_iclerk:clerk_compact(State#state.clerk, - Checker, - InitiateFun, - CloseFun, - FilterFun, - Manifest), - {reply, {ok, Clerk}, State#state{compaction_pending=true}} - end; + Manifest = leveled_imanifest:to_list(State#state.manifest), + leveled_iclerk:clerk_compact(State#state.clerk, + Checker, + InitiateFun, + CloseFun, + FilterFun, + Manifest), + {reply, {ok, Clerk}, State#state{compaction_pending=true}}; handle_call(compaction_pending, _From, State) -> {reply, State#state.compaction_pending, State}; handle_call({trim, PersistedSQN}, _From, State) -> @@ -1230,9 +1225,7 @@ filepath(CompactFilePath, NewSQN, compact_journal) -> ++ "." ++ ?PENDING_FILEX). -initiate_penciller_snapshot(Bookie) -> - {ok, LedgerSnap, _} = - leveled_bookie:book_snapshot(Bookie, ledger, undefined, true), +initiate_penciller_snapshot(LedgerSnap) -> MaxSQN = leveled_penciller:pcl_getstartupsequencenumber(LedgerSnap), {LedgerSnap, MaxSQN}. diff --git a/src/leveled_log.erl b/src/leveled_log.erl index 954e3c0..8c0837a 100644 --- a/src/leveled_log.erl +++ b/src/leveled_log.erl @@ -392,8 +392,11 @@ ++ "with totals of cycle_count=~w " ++ "fetch_time=~w index_time=~w"}}, {"CDB20", - {warn, "Error ~w caught when safe reading a file to length ~w"}} - ]). + {warn, "Error ~w caught when safe reading a file to length ~w"}}, + {"CDB21", + {warn, "File ~s to be deleted but already gone"}} + + ]). %%%============================================================================ diff --git a/test/leveledjc_eqc.erl b/test/leveledjc_eqc.erl index 18c80c8..b188b2d 100644 --- a/test/leveledjc_eqc.erl +++ b/test/leveledjc_eqc.erl @@ -94,8 +94,8 @@ init_backend_adapt(S, [Tag, Options, Name]) -> %% @doc init_backend - The actual operation %% Start the database and read data from disk init_backend(_Tag, Options, Name) -> - % Options0 = proplists:delete(log_level, Options), - case leveled_bookie:book_start(Options) of + Options0 = proplists:delete(log_level, Options), + case leveled_bookie:book_start(Options0) of {ok, Bookie} -> unlink(Bookie), erlang:register(Name, Bookie), @@ -570,33 +570,9 @@ kill_next(S, Value, [Pid]) -> -%% --- Operation: compactisalive --- - -compactisalive_pre(S) -> - is_leveled_open(S) andalso maps:get(iclerk, S, undefined) /= undefined. - -compactisalive_args(#{iclerk := IClerk}) -> - [IClerk]. - -compactisalive_pre(#{iclerk := Pid}, [IClerk]) -> - Pid == IClerk. - -compactisalive(IClerk) -> - is_process_alive(IClerk). - -compactisalive_post(_S, [_IClerk], Res) -> - Res. - %% --- Operation: compacthappened --- -compacthappened_pre(S) -> - is_leveled_open(S) andalso maps:get(iclerk, S, undefined) /= undefined. - -%% Commenting out args disables the operation -%% compacthappened_args(#{dir := DataDir}) -> -%% [DataDir]. - compacthappened(DataDir) -> PostCompact = filename:join(DataDir, "journal/journal_files/post_compact"), case filelib:is_dir(PostCompact) of @@ -627,10 +603,6 @@ journalwritten(DataDir) -> [] end. -compacthappened_post(_S, [_DataDir], Res) -> - eq(Res, []). - - %% --- Operation: compact journal --- @@ -647,27 +619,27 @@ compact_adapt(#{leveled := Leveled}, [_Pid, TS]) -> [ Leveled, TS ]. compact(Pid, TS) -> - {ok, IClerk} = leveled_bookie:book_eqccompactjournal(Pid, TS), - IClerk. + {R, _IClerk} = leveled_bookie:book_eqccompactjournal(Pid, TS), + R. -compact_next(S, IClerk, [_Pid, _TS]) -> - case maps:get(iclerk, S, undefined) of - undefined -> - S#{iclerk => IClerk}; +compact_next(S, R, [_Pid, _TS]) -> + case {R, maps:get(previous_compact, S, undefined)} of + {ok, undefined} -> + S#{previous_compact => true}; _ -> S end. compact_post(S, [_Pid, _TS], Res) -> - case maps:get(iclerk, S, undefined) of - undefined -> - is_pid(Res); - IClerk -> - IClerk == Res + case Res of + ok -> + true; + busy -> + true == maps:get(previous_compact, S, undefined) end. compact_features(S, [_Pid, _TS], _Res) -> - case maps:get(iclerk, S, undefined) of + case maps:get(previous_compact, S, undefined) of undefined -> [{compact, fresh}]; _ -> @@ -1054,12 +1026,16 @@ prop_db() -> CompactionFiles = compacthappened(Dir), LedgerFiles = ledgerpersisted(Dir), JournalFiles = journalwritten(Dir), - io:format("File counts: Compacted ~w Journal ~w Ledger ~w~n", [length(CompactionFiles), length(LedgerFiles), length(JournalFiles)]), + % io:format("File counts: Compacted ~w Journal ~w Ledger ~w~n", [length(CompactionFiles), length(LedgerFiles), length(JournalFiles)]), + case whereis(maps:get(sut, initial_state())) of - undefined -> delete_level_data(Dir); + undefined -> + % io:format("Init state undefined - deleting~n"), + delete_level_data(Dir); Pid when is_pid(Pid) -> + % io:format("Init state defined - destroying~n"), leveled_bookie:book_destroy(Pid) end, @@ -1236,18 +1212,14 @@ in_head_only_mode(S) -> wait_for_procs(Known, Timeout) -> case erlang:processes() -- Known of [] -> []; - Running -> + _NonEmptyList -> if - Timeout > 100 -> - timer:sleep(100), - wait_for_procs(Known, Timeout - 100); - Timeout >= 0 -> - lists:map(fun(P) -> io:format("********* Sending timeout to ~w *********~n", [P]), gen_fsm:send_event(P, timeout) end, Running), + Timeout > 0 -> timer:sleep(100), wait_for_procs(Known, Timeout - 100); true -> - lists:foreach(fun(P) -> io:format("Process info : ~w~n", [process_info(P)]) end, Running), - Running + timer:sleep(10000), + erlang:processes() -- Known end end. From f907fb5c97c7f9bee4af5d2bd975b1176992f842 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 25 Jan 2019 19:27:42 +0000 Subject: [PATCH 11/15] Close in all cases in leveled_imanifest --- src/leveled_imanifest.erl | 1 + test/leveledjc_eqc.erl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/leveled_imanifest.erl b/src/leveled_imanifest.erl index 24da2b8..1f431fe 100644 --- a/src/leveled_imanifest.erl +++ b/src/leveled_imanifest.erl @@ -57,6 +57,7 @@ generate_entry(Journal) -> ok = leveled_cdb:cdb_close(PidR), [{StartSQN, NewFN, PidR, LastKey}]; empty -> + ok = leveled_cdb:cdb_close(PidR), leveled_log:log("IC013", [NewFN]), [] end. diff --git a/test/leveledjc_eqc.erl b/test/leveledjc_eqc.erl index b188b2d..21639eb 100644 --- a/test/leveledjc_eqc.erl +++ b/test/leveledjc_eqc.erl @@ -93,8 +93,8 @@ init_backend_adapt(S, [Tag, Options, Name]) -> %% @doc init_backend - The actual operation %% Start the database and read data from disk -init_backend(_Tag, Options, Name) -> - Options0 = proplists:delete(log_level, Options), +init_backend(_Tag, Options0, Name) -> + % Options0 = proplists:delete(log_level, Options), case leveled_bookie:book_start(Options0) of {ok, Bookie} -> unlink(Bookie), From ae9b03ab3c3c9932398dd4846fdcf774abbd3d00 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Sat, 26 Jan 2019 16:57:25 +0000 Subject: [PATCH 12/15] Fix unit tests - and make slot size configurable --- include/leveled.hrl | 3 ++- src/leveled_bookie.erl | 21 ++++++++++++--------- src/leveled_cdb.erl | 20 +++++++++++++++++++- src/leveled_inker.erl | 40 ++++++++++++++++++++++------------------ src/leveled_sst.erl | 32 ++++++++++++++++++-------------- test/leveledjc_eqc.erl | 6 +++--- 6 files changed, 76 insertions(+), 46 deletions(-) diff --git a/include/leveled.hrl b/include/leveled.hrl index a945f0c..9357bd5 100644 --- a/include/leveled.hrl +++ b/include/leveled.hrl @@ -50,7 +50,8 @@ {press_method = native :: leveled_sst:press_method(), log_options = leveled_log:get_opts() - :: leveled_log:log_options()}). + :: leveled_log:log_options(), + max_sstslots = 256 :: pos_integer()}). -record(inker_options, {cdb_max_size :: integer() | undefined, diff --git a/src/leveled_bookie.erl b/src/leveled_bookie.erl index 02b66e7..11a5312 100644 --- a/src/leveled_bookie.erl +++ b/src/leveled_bookie.erl @@ -62,7 +62,6 @@ book_headonly/4, book_snapshot/4, book_compactjournal/2, - book_eqccompactjournal/2, book_islastcompactionpending/1, book_trimjournal/1, book_hotbackup/1, @@ -101,8 +100,8 @@ -include_lib("eunit/include/eunit.hrl"). -define(CACHE_SIZE, 2500). --define(MIN_CACHE_SIZE, 1). --define(MIN_PCL_CACHE_SIZE, 4). +-define(MIN_CACHE_SIZE, 100). +-define(MIN_PCL_CACHE_SIZE, 400). -define(MAX_PCL_CACHE_SIZE, 28000). % This is less than actual max - but COIN_SIDECOUNT -define(CACHE_SIZE_JITTER, 25). @@ -125,6 +124,7 @@ {snapshot_bookie, undefined}, {cache_size, ?CACHE_SIZE}, {max_journalsize, 1000000000}, + {max_sstslots, 256}, {sync_strategy, none}, {head_only, false}, {waste_retention_period, undefined}, @@ -221,6 +221,10 @@ {max_journalsize, pos_integer()} | % The maximum size of a journal file in bytes. The abolute % maximum must be 4GB due to 4 byte file pointers being used + {max_sstslots, pos_integer()} | + % The maximum number of slots in a SST file. All testing is done + % at a size of 256 (except for Quickcheck tests}, altering this + % value is not recommended {sync_strategy, sync_mode()} | % Should be sync if it is necessary to flush to disk after every % write, or none if not (allow the OS to schecdule). This has a @@ -1006,7 +1010,6 @@ book_snapshot(Pid, SnapType, Query, LongRunning) -> -spec book_compactjournal(pid(), integer()) -> ok|busy. --spec book_eqccompactjournal(pid(), integer()) -> {ok|busy, pid()|undefined}. -spec book_islastcompactionpending(pid()) -> boolean(). -spec book_trimjournal(pid()) -> ok. @@ -1015,9 +1018,6 @@ book_snapshot(Pid, SnapType, Query, LongRunning) -> %% the scheduling of Journla compaction is called externally, so it is assumed %% in Riak it will be triggered by a vnode callback. -book_eqccompactjournal(Pid, Timeout) -> - gen_server:call(Pid, {compact_journal, Timeout}, infinity). - book_compactjournal(Pid, Timeout) -> {R, _P} = gen_server:call(Pid, {compact_journal, Timeout}, infinity), R. @@ -1639,6 +1639,8 @@ set_options(Opts) -> % If using lz4 this is not recommended false end, + + MaxSSTSlots = proplists:get_value(max_sstslots, Opts), {#inker_options{root_path = JournalFP, reload_strategy = ReloadStrategy, @@ -1660,8 +1662,9 @@ set_options(Opts) -> snaptimeout_short = SnapTimeoutShort, snaptimeout_long = SnapTimeoutLong, sst_options = - #sst_options{press_method = CompressionMethod, - log_options=leveled_log:get_opts()}} + #sst_options{press_method=CompressionMethod, + log_options=leveled_log:get_opts(), + max_sstslots=MaxSSTSlots}} }. diff --git a/src/leveled_cdb.erl b/src/leveled_cdb.erl index 1693ed3..8d8dcb0 100644 --- a/src/leveled_cdb.erl +++ b/src/leveled_cdb.erl @@ -820,7 +820,7 @@ finished_rolling(CDB) -> -spec close_pendingdelete(file:io_device(), list(), list()|undefined) -> ok. %% @doc -%% If delete is pending - thent he close behaviour needs to actuallly delete +%% If delete is pending - then the close behaviour needs to actuallly delete %% the file close_pendingdelete(Handle, Filename, WasteFP) -> ok = file:close(Handle), @@ -2606,6 +2606,24 @@ badly_written_test() -> ok = cdb_close(P2), file:delete(F1). +pendingdelete_test() -> + F1 = "test/test_area/deletfile_test.pnd", + file:delete(F1), + {ok, P1} = cdb_open_writer(F1, #cdb_options{binary_mode=false}), + KVList = generate_sequentialkeys(1000, []), + ok = cdb_mput(P1, KVList), + ?assertMatch(probably, cdb_keycheck(P1, "Key1")), + ?assertMatch({"Key1", "Value1"}, cdb_get(P1, "Key1")), + ?assertMatch({"Key100", "Value100"}, cdb_get(P1, "Key100")), + {ok, F2} = cdb_complete(P1), + {ok, P2} = cdb_open_reader(F2, #cdb_options{binary_mode=false}), + ?assertMatch({"Key1", "Value1"}, cdb_get(P2, "Key1")), + ?assertMatch({"Key100", "Value100"}, cdb_get(P2, "Key100")), + file:delete(F2), + ok = cdb_deletepending(P2), + % No issues destroying even though the file has already been removed + ok = cdb_destroy(P2). + nonsense_coverage_test() -> {ok, Pid} = gen_fsm:start_link(?MODULE, [#cdb_options{}], []), diff --git a/src/leveled_inker.erl b/src/leveled_inker.erl index 379f417..4044ff7 100644 --- a/src/leveled_inker.erl +++ b/src/leveled_inker.erl @@ -1431,22 +1431,26 @@ compact_journal_testto(WRP, ExpectedFiles) -> ActualManifest = ink_getmanifest(Ink1), ok = ink_printmanifest(Ink1), ?assertMatch(3, length(ActualManifest)), - ok = ink_compactjournal(Ink1, - Checker, - fun(X) -> {X, 55} end, - fun(_F) -> ok end, - fun(L, K, SQN) -> lists:member({SQN, K}, L) end, - 5000), + {ok, _ICL1} = ink_compactjournal(Ink1, + Checker, + fun(X) -> {X, 55} end, + fun(_F) -> ok end, + fun(L, K, SQN) -> + lists:member({SQN, K}, L) + end, + 5000), timer:sleep(1000), CompactedManifest1 = ink_getmanifest(Ink1), ?assertMatch(2, length(CompactedManifest1)), Checker2 = lists:sublist(Checker, 16), - ok = ink_compactjournal(Ink1, - Checker2, - fun(X) -> {X, 55} end, - fun(_F) -> ok end, - fun(L, K, SQN) -> lists:member({SQN, K}, L) end, - 5000), + {ok, _ICL2} = ink_compactjournal(Ink1, + Checker2, + fun(X) -> {X, 55} end, + fun(_F) -> ok end, + fun(L, K, SQN) -> + lists:member({SQN, K}, L) + end, + 5000), timer:sleep(1000), CompactedManifest2 = ink_getmanifest(Ink1), {ok, PrefixTest} = re:compile(?COMPACT_FP), @@ -1475,12 +1479,12 @@ empty_manifest_test() -> CheckFun = fun(L, K, SQN) -> lists:member({SQN, key_converter(K)}, L) end, ?assertMatch(false, CheckFun([], "key", 1)), - ok = ink_compactjournal(Ink1, - [], - fun(X) -> {X, 55} end, - fun(_F) -> ok end, - CheckFun, - 5000), + {ok, _ICL1} = ink_compactjournal(Ink1, + [], + fun(X) -> {X, 55} end, + fun(_F) -> ok end, + CheckFun, + 5000), timer:sleep(1000), ?assertMatch(1, length(ink_getmanifest(Ink1))), ok = ink_close(Ink1), diff --git a/src/leveled_sst.erl b/src/leveled_sst.erl index efe4441..3e7b156 100644 --- a/src/leveled_sst.erl +++ b/src/leveled_sst.erl @@ -72,7 +72,6 @@ -include("include/leveled.hrl"). --define(MAX_SLOTS, 2). -define(LOOK_SLOTSIZE, 128). % Maximum of 128 -define(LOOK_BLOCKSIZE, {24, 32}). % 4x + y = ?LOOK_SLOTSIZE -define(NOLOOK_SLOTSIZE, 256). @@ -258,7 +257,7 @@ sst_new(RootPath, Filename, Level, KVList, MaxSQN, OptsSST, IndexModDate) -> PressMethod0 = compress_level(Level, OptsSST#sst_options.press_method), OptsSST0 = OptsSST#sst_options{press_method = PressMethod0}, {[], [], SlotList, FK} = - merge_lists(KVList, PressMethod0, IndexModDate), + merge_lists(KVList, OptsSST0, IndexModDate), case gen_fsm:sync_send_event(Pid, {sst_new, RootPath, @@ -309,7 +308,7 @@ sst_new(RootPath, Filename, OptsSST0 = OptsSST#sst_options{press_method = PressMethod0}, {Rem1, Rem2, SlotList, FK} = merge_lists(KVL1, KVL2, {IsBasement, Level}, - PressMethod0, IndexModDate), + OptsSST0, IndexModDate), case SlotList of [] -> empty; @@ -499,7 +498,7 @@ starting({sst_newlevelzero, RootPath, Filename, SW1 = os:timestamp(), {[], [], SlotList, FirstKey} = - merge_lists(KVList, PressMethod, IdxModDate), + merge_lists(KVList, OptsSST, IdxModDate), Time1 = timer:now_diff(os:timestamp(), SW1), SW2 = os:timestamp(), @@ -2131,16 +2130,17 @@ revert_position(Pos) -> %% there are matching keys then the highest sequence number must be chosen and %% any lower sequence numbers should be compacted out of existence --spec merge_lists(list(), press_method(), boolean()) +-spec merge_lists(list(), sst_options(), boolean()) -> {list(), list(), list(tuple()), tuple()|null}. %% @doc %% %% Merge from asingle list (i.e. at Level 0) -merge_lists(KVList1, PressMethod, IdxModDate) -> +merge_lists(KVList1, SSTOpts, IdxModDate) -> SlotCount = length(KVList1) div ?LOOK_SLOTSIZE, {[], [], - split_lists(KVList1, [], SlotCount, PressMethod, IdxModDate), + split_lists(KVList1, [], + SlotCount, SSTOpts#sst_options.press_method, IdxModDate), element(1, lists:nth(1, KVList1))}. @@ -2157,33 +2157,34 @@ split_lists(KVList1, SlotLists, N, PressMethod, IdxModDate) -> split_lists(KVListRem, [SlotD|SlotLists], N - 1, PressMethod, IdxModDate). --spec merge_lists(list(), list(), tuple(), press_method(), boolean()) -> +-spec merge_lists(list(), list(), tuple(), sst_options(), boolean()) -> {list(), list(), list(tuple()), tuple()|null}. %% @doc %% Merge lists when merging across more thna one file. KVLists that are %% provided may include pointers to fetch more Keys/Values from the source %% file -merge_lists(KVList1, KVList2, LevelInfo, PressMethod, IndexModDate) -> +merge_lists(KVList1, KVList2, LevelInfo, SSTOpts, IndexModDate) -> merge_lists(KVList1, KVList2, LevelInfo, [], null, 0, - PressMethod, + SSTOpts#sst_options.max_sstslots, + SSTOpts#sst_options.press_method, IndexModDate, #build_timings{}). -merge_lists(KVL1, KVL2, LI, SlotList, FirstKey, ?MAX_SLOTS, +merge_lists(KVL1, KVL2, LI, SlotList, FirstKey, MaxSlots, MaxSlots, _PressMethod, _IdxModDate, T0) -> % This SST file is full, move to complete file, and return the % remainder log_buildtimings(T0, LI), {KVL1, KVL2, lists:reverse(SlotList), FirstKey}; -merge_lists([], [], LI, SlotList, FirstKey, _SlotCount, +merge_lists([], [], LI, SlotList, FirstKey, _SlotCount, _MaxSlots, _PressMethod, _IdxModDate, T0) -> % the source files are empty, complete the file log_buildtimings(T0, LI), {[], [], lists:reverse(SlotList), FirstKey}; -merge_lists(KVL1, KVL2, LI, SlotList, FirstKey, SlotCount, +merge_lists(KVL1, KVL2, LI, SlotList, FirstKey, SlotCount, MaxSlots, PressMethod, IdxModDate, T0) -> % Form a slot by merging the two lists until the next 128 K/V pairs have % been determined @@ -2200,6 +2201,7 @@ merge_lists(KVL1, KVL2, LI, SlotList, FirstKey, SlotCount, SlotList, FK0, SlotCount, + MaxSlots, PressMethod, IdxModDate, T1); @@ -2214,6 +2216,7 @@ merge_lists(KVL1, KVL2, LI, SlotList, FirstKey, SlotCount, [SlotD|SlotList], FK0, SlotCount + 1, + MaxSlots, PressMethod, IdxModDate, T2) @@ -2560,7 +2563,8 @@ merge_tombstonelist_test() -> R = merge_lists([SkippingKV1, SkippingKV3, SkippingKV5], [SkippingKV2, SkippingKV4], {true, 9999999}, - native, + #sst_options{press_method = native, + max_sstslots = 256}, ?INDEX_MODDATE), ?assertMatch({[], [], [], null}, R). diff --git a/test/leveledjc_eqc.erl b/test/leveledjc_eqc.erl index 21639eb..c6bcb8c 100644 --- a/test/leveledjc_eqc.erl +++ b/test/leveledjc_eqc.erl @@ -76,7 +76,8 @@ init_backend_args(#{dir := Dir, sut := Name} = S) -> case maps:get(start_opts, S, undefined) of undefined -> [ default(?RIAK_TAG, ?STD_TAG), %% Just test one tag at a time - [{root_path, Dir}, {log_level, error}, {cache_size, 10}, {max_pencillercachesize, 40}, {max_journalsize, 20000} | gen_opts()], Name ]; + [{root_path, Dir}, {log_level, error}, + {max_sstslots, 2}, {cache_size, 10}, {max_pencillercachesize, 40}, {max_journalsize, 20000} | gen_opts()], Name ]; Opts -> %% root_path is part of existing options [ maps:get(tag, S), Opts, Name ] @@ -619,8 +620,7 @@ compact_adapt(#{leveled := Leveled}, [_Pid, TS]) -> [ Leveled, TS ]. compact(Pid, TS) -> - {R, _IClerk} = leveled_bookie:book_eqccompactjournal(Pid, TS), - R. + leveled_bookie:book_compactjournal(Pid, TS). compact_next(S, R, [_Pid, _TS]) -> case {R, maps:get(previous_compact, S, undefined)} of From 8f6862a10b2ee13b2bde74c6b25161d22c23f26a Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Sun, 27 Jan 2019 22:03:55 +0000 Subject: [PATCH 13/15] Test sst slot configuration change Confirm it results in many more files, if the slot count reduced. Has to handle the fact that Level 0 file has unlimited slots regardless of number of slots configured --- test/end_to_end/basic_SUITE.erl | 55 +++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/test/end_to_end/basic_SUITE.erl b/test/end_to_end/basic_SUITE.erl index a17108a..12cda7e 100644 --- a/test/end_to_end/basic_SUITE.erl +++ b/test/end_to_end/basic_SUITE.erl @@ -12,21 +12,23 @@ is_empty_test/1, many_put_fetch_switchcompression/1, bigjournal_littlejournal/1, + bigsst_littlesst/1, safereaderror_startup/1, remove_journal_test/1 ]). all() -> [ - simple_put_fetch_head_delete, - many_put_fetch_head, - journal_compaction, - fetchput_snapshot, - load_and_count, - load_and_count_withdelete, - space_clear_ondelete, - is_empty_test, - many_put_fetch_switchcompression, - bigjournal_littlejournal, + % simple_put_fetch_head_delete, + % many_put_fetch_head, + % journal_compaction, + % fetchput_snapshot, + % load_and_count, + % load_and_count_withdelete, + % space_clear_ondelete, + % is_empty_test, + % many_put_fetch_switchcompression, + % bigjournal_littlejournal, + bigsst_littlesst, safereaderror_startup, remove_journal_test ]. @@ -164,6 +166,39 @@ bigjournal_littlejournal(_Config) -> ok = leveled_bookie:book_destroy(Bookie2). +bigsst_littlesst(_Config) -> + RootPath = testutil:reset_filestructure(), + StartOpts1 = [{root_path, RootPath}, + {max_journalsize, 50000000}, + {cache_size, 1000}, + {max_pencillercachesize, 16000}, + {max_sstslots, 256}, + {sync_strategy, testutil:sync_strategy()}, + {compression_point, on_compact}], + {ok, Bookie1} = leveled_bookie:book_start(StartOpts1), + ObjL1 = + testutil:generate_objects(60000, 1, [], + leveled_rand:rand_bytes(100), + fun() -> [] end, <<"B">>), + testutil:riakload(Bookie1, ObjL1), + testutil:check_forlist(Bookie1, ObjL1), + JFP = RootPath ++ "/ledger/ledger_files/", + {ok, FNS1} = file:list_dir(JFP), + ok = leveled_bookie:book_destroy(Bookie1), + + + StartOpts2 = lists:ukeysort(1, [{max_sstslots, 24}|StartOpts1]), + {ok, Bookie2} = leveled_bookie:book_start(StartOpts2), + testutil:riakload(Bookie2, ObjL1), + testutil:check_forlist(Bookie2, ObjL1), + {ok, FNS2} = file:list_dir(JFP), + ok = leveled_bookie:book_destroy(Bookie2), + io:format("Big SST ~w files Little SST ~w files~n", + [length(FNS1), length(FNS2)]), + true = length(FNS2) > (2 * length(FNS1)). + + + journal_compaction(_Config) -> journal_compaction_tester(false, 3600), journal_compaction_tester(false, undefined), From db0db67c45585b67d7df73d668b223ba5461f583 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Sun, 27 Jan 2019 22:09:48 +0000 Subject: [PATCH 14/15] Delete leveledjc_eqc.erl Remove this for now, until issues with running tests without QC installed can be resolved. Allow for changes to support QC to be merged into master. --- test/leveledjc_eqc.erl | 1235 ---------------------------------------- 1 file changed, 1235 deletions(-) delete mode 100644 test/leveledjc_eqc.erl diff --git a/test/leveledjc_eqc.erl b/test/leveledjc_eqc.erl deleted file mode 100644 index c6bcb8c..0000000 --- a/test/leveledjc_eqc.erl +++ /dev/null @@ -1,1235 +0,0 @@ -%% ------------------------------------------------------------------- -%% -%% leveld_eqc: basic statem for doing things to leveled -%% -%% 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(leveledjc_eqc). - --include_lib("eqc/include/eqc.hrl"). --include_lib("eqc/include/eqc_statem.hrl"). --include_lib("eunit/include/eunit.hrl"). --include("../include/leveled.hrl"). - --compile([export_all, nowarn_export_all, {nowarn_deprecated_function, - [{gen_fsm, send_event, 2}]}]). - --define(NUMTESTS, 1000). --define(QC_OUT(P), - eqc:on_output(fun(Str, Args) -> - io:format(user, Str, Args) end, P)). - --define(CMD_VALID(State, Cmd, True, False), - case is_valid_cmd(State, Cmd) of - true -> True; - false -> False - end). - - -eqc_test_() -> - Timeout = 50, - {timeout, max(2 * Timeout, Timeout + 10), - ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(Timeout, ?QC_OUT(prop_db()))))}. - -run() -> - run(?NUMTESTS). - -run(Count) -> - eqc:quickcheck(eqc:numtests(Count, prop_db())). - -check() -> - eqc:check(prop_db()). - -iff(B1, B2) -> B1 == B2. -implies(B1, B2) -> (not B1 orelse B2). - -%% start_opts should not be added to this map, it is added only when the system is started the first time. -initial_state() -> - #{dir => {var, dir}, - sut => sut, - leveled => undefined, %% to make adapt happy after failing pre/1 - counter => 0, - model => orddict:new(), - previous_keys => [], - deleted_keys => [], - folders => [] - }. - -%% --- Operation: init_backend --- -init_backend_pre(S) -> - not is_leveled_open(S). - -init_backend_args(#{dir := Dir, sut := Name} = S) -> - case maps:get(start_opts, S, undefined) of - undefined -> - [ default(?RIAK_TAG, ?STD_TAG), %% Just test one tag at a time - [{root_path, Dir}, {log_level, error}, - {max_sstslots, 2}, {cache_size, 10}, {max_pencillercachesize, 40}, {max_journalsize, 20000} | gen_opts()], Name ]; - Opts -> - %% root_path is part of existing options - [ maps:get(tag, S), Opts, Name ] - end. - -init_backend_pre(S, [Tag, Options, _]) -> - %% for shrinking - PreviousOptions = maps:get(start_opts, S, undefined), - maps:get(tag, S, Tag) == Tag andalso - PreviousOptions == undefined orelse PreviousOptions == Options. - -init_backend_adapt(S, [Tag, Options, Name]) -> - [ maps:get(tag, S, Tag), maps:get(start_opts, S, Options), Name]. - -%% @doc init_backend - The actual operation -%% Start the database and read data from disk -init_backend(_Tag, Options0, Name) -> - % Options0 = proplists:delete(log_level, Options), - case leveled_bookie:book_start(Options0) of - {ok, Bookie} -> - unlink(Bookie), - erlang:register(Name, Bookie), - Bookie; - Error -> Error - end. - -init_backend_next(S, LevelEdPid, [Tag, Options, _]) -> - S#{leveled => LevelEdPid, start_opts => Options, tag => Tag}. - -init_backend_post(_S, [_, _Options, _], LevelEdPid) -> - is_pid(LevelEdPid). - -init_backend_features(_S, [_Tag, Options, _], _Res) -> - [{start_options, Options}]. - - -%% --- Operation: stop --- -stop_pre(S) -> - is_leveled_open(S). - -%% @doc stop_args - Argument generator -stop_args(#{leveled := Pid}) -> - [Pid]. - -stop_pre(#{leveled := Leveled}, [Pid]) -> - %% check during shrinking - Pid == Leveled. - -stop_adapt(#{leveled := Leveled}, [_]) -> - [Leveled]. - -%% @doc stop - The actual operation -%% Stop the server, but the values are still on disk -stop(Pid) -> - ok = leveled_bookie:book_close(Pid). - -stop_next(S, _Value, [_Pid]) -> - S#{leveled => undefined, - iclerk => undefined, - folders => [], - used_folders => [], - stop_folders => maps:get(folders, S, []) ++ maps:get(used_folders, S, [])}. - -stop_post(_S, [Pid], _Res) -> - Mon = erlang:monitor(process, Pid), - receive - {'DOWN', Mon, _Type, Pid, _Info} -> - true - after 5000 -> - {still_a_pid, Pid} - end. - - -%% --- Operation: updateload --- -updateload_pre(S) -> - is_leveled_open(S). - -%% updateload for specific bucket (doesn't overlap with get/put/delete) -updateload_args(#{leveled := Pid, tag := Tag}) -> - ?LET(Categories, gen_categories(Tag), - ?LET({{Key, Bucket}, Value, IndexSpec, MetaData}, - {{gen_key(), <<"LoadBucket">>}, gen_val(), [{add, Cat, gen_index_value()} || Cat <- Categories ], []}, - case Tag of - ?STD_TAG -> [Pid, Bucket, Key, Value, Value, IndexSpec, Tag, MetaData]; - ?RIAK_TAG -> - Obj = testutil:riak_object(Bucket, Key, Value, MetaData), %% this has a side effect inserting a random nr - [Pid, Bucket, Key, Value, Obj, IndexSpec, Tag, MetaData] - end - )). - -updateload_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Value, _Obj, _, _, _]) -> - Pid == Leveled. - -updateload_adapt(#{leveled := Leveled}, [_, Bucket, Key, Value, Obj, Spec, Tag, MetaData]) -> - [ Leveled, Bucket, Key, Value, Obj, Spec, Tag, MetaData ]. - -%% @doc put - The actual operation -updateload(Pid, Bucket, Key, Value, Obj, Spec, Tag, MetaData) -> - Values = - case Tag of - ?STD_TAG -> multiply(100, Value); - ?RIAK_TAG -> - lists:map(fun(V) -> testutil:riak_object(Bucket, Key, V, MetaData) end, - multiply(100, Value)) - end ++ [Obj], - lists:map(fun(V) -> leveled_bookie:book_put(Pid, Bucket, Key, V, Spec, Tag) end, Values). - -multiply(1, _Value) -> - []; -multiply(N, Value) when N > 1 -> - <> = Value, - NewValue = <>, - [NewValue | multiply(N-1, NewValue)]. - -updateload_next(#{model := Model} = S, _V, [_Pid, Bucket, Key, _Value, Obj, Spec, _Tag, _MetaData]) -> - ?CMD_VALID(S, put, - begin - NewSpec = - case orddict:find({Bucket, Key}, Model) of - error -> merge_index_spec([], Spec); - {ok, {_, OldSpec}} -> - merge_index_spec(OldSpec, Spec) - end, - S#{model => orddict:store({Bucket, Key}, {Obj, NewSpec}, Model)} - end, - S). - -updateload_post(S, [_, _, _, _, _, _, _, _], Results) -> - lists:all(fun(Res) -> ?CMD_VALID(S, put, Res == ok, Res == {unsupported_message, put}) end, Results). - -updateload_features(#{previous_keys := PK} = S, [_Pid, Bucket, Key, _Value, _Obj, _, Tag, _], _Res) -> - ?CMD_VALID(S, put, - case - lists:member({Key, Bucket}, PK) of - true -> - [{updateload, update, Tag}]; - false -> - [{updateload, insert, Tag}] - end, - [{updateload, unsupported}]). - - -%% --- Operation: put --- -put_pre(S) -> - is_leveled_open(S). - -put_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> - ?LET(Categories, gen_categories(Tag), - ?LET({{Key, Bucket}, Value, IndexSpec, MetaData}, - {gen_key_in_bucket(PK), gen_val(), [{add, Cat, gen_index_value()} || Cat <- Categories ], []}, - case Tag of - ?STD_TAG -> [Pid, Bucket, Key, Value, IndexSpec, elements([none, Tag])]; - ?RIAK_TAG -> - Obj = testutil:riak_object(Bucket, Key, Value, MetaData), - [Pid, Bucket, Key, Obj, IndexSpec, Tag] - end)). - -put_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Value, _, _]) -> - Pid == Leveled. - -put_adapt(#{leveled := Leveled}, [_, Bucket, Key, Value, Spec, Tag]) -> - [ Leveled, Bucket, Key, Value, Spec, Tag ]. - -%% @doc put - The actual operation -put(Pid, Bucket, Key, Value, Spec, none) -> - leveled_bookie:book_put(Pid, Bucket, Key, Value, Spec); -put(Pid, Bucket, Key, Value, Spec, Tag) -> - leveled_bookie:book_put(Pid, Bucket, Key, Value, Spec, Tag). - -put_next(#{model := Model, previous_keys := PK} = S, _Value, [_Pid, Bucket, Key, Value, Spec, _Tag]) -> - ?CMD_VALID(S, put, - begin - NewSpec = - case orddict:find({Bucket, Key}, Model) of - error -> merge_index_spec([], Spec); - {ok, {_, OldSpec}} -> - merge_index_spec(OldSpec, Spec) - end, - S#{model => orddict:store({Bucket, Key}, {Value, NewSpec}, Model), - previous_keys => PK ++ [{Key, Bucket}]} - end, - S). - -put_post(S, [_, _, _, _, _, _], Res) -> - ?CMD_VALID(S, put, eq(Res, ok), eq(Res, {unsupported_message, put})). - -put_features(#{previous_keys := PK} = S, [_Pid, Bucket, Key, _Value, _, Tag], _Res) -> - ?CMD_VALID(S, put, - case - lists:member({Key, Bucket}, PK) of - true -> - [{put, update, Tag}]; - false -> - [{put, insert, Tag}] - end, - [{put, unsupported}]). - -merge_index_spec(Spec, []) -> - Spec; -merge_index_spec(Spec, [{add, Cat, Idx} | Rest]) -> - merge_index_spec(lists:delete({Cat, Idx}, Spec) ++ [{Cat, Idx}], Rest); -merge_index_spec(Spec, [{remove, Cat, Idx} | Rest]) -> - merge_index_spec(lists:delete({Cat, Idx}, Spec), Rest). - - -%% --- Operation: get --- -get_pre(S) -> - is_leveled_open(S). - -get_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> - ?LET({Key, Bucket}, gen_key_in_bucket(PK), - [Pid, Bucket, Key, case Tag of ?STD_TAG -> default(none, Tag); _ -> Tag end]). - -%% @doc get - The actual operation -get(Pid, Bucket, Key, none) -> - leveled_bookie:book_get(Pid, Bucket, Key); -get(Pid, Bucket, Key, Tag) -> - leveled_bookie:book_get(Pid, Bucket, Key, Tag). - -get_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Tag]) -> - Pid == Leveled. - -get_adapt(#{leveled := Leveled}, [_, Bucket, Key, Tag]) -> - [Leveled, Bucket, Key, Tag]. - -get_post(#{model := Model} = S, [_Pid, Bucket, Key, Tag], Res) -> - ?CMD_VALID(S, get, - case Res of - {ok, _} -> - {ok, {Value, _}} = orddict:find({Bucket, Key}, Model), - eq(Res, {ok, Value}); - not_found -> - %% Weird to be able to supply a tag, but must be STD_TAG... - Tag =/= ?STD_TAG orelse orddict:find({Bucket, Key}, Model) == error - end, - eq(Res, {unsupported_message, get})). - -get_features(#{deleted_keys := DK, previous_keys := PK}, [_Pid, Bucket, Key, _Tag], Res) -> - case Res of - not_found -> - [{get, not_found, deleted} || lists:member({Key, Bucket}, DK)] ++ - [{get, not_found, not_inserted} || not lists:member({Key, Bucket}, PK)]; - {ok, B} when is_binary(B) -> - [{get, found}]; - {unsupported_message, _} -> - [{get, unsupported}] - end. - -%% --- Operation: mput --- -mput_pre(S) -> - is_leveled_open(S). - -%% @doc mput_args - Argument generator -%% Specification says: duplicated should be removed -%% "%% The list should be de-duplicated before it is passed to the bookie." -%% Wether this means that keys should be unique or even Action and values is unclear. -%% Slack discussion: -%% `[{add, B1, K1, SK1}, {add, B1, K1, SK2}]` should be fine (same bucket and key, different subkey) -%% -%% Really weird to have to specify a value in case of a remove action -mput_args(#{leveled := Pid, previous_keys := PK}) -> - ?LET(Objs, list({gen_key_in_bucket(PK), nat()}), - [Pid, [ {weighted_default({5, add}, {1, remove}), Bucket, Key, SubKey, gen_val()} || {{Key, Bucket}, SubKey} <- Objs ]]). - - -mput_pre(#{leveled := Leveled}, [Pid, ObjSpecs]) -> - Pid == Leveled andalso no_key_dups(ObjSpecs) == ObjSpecs. - -mput_adapt(#{leveled := Leveled}, [_, ObjSpecs]) -> - [ Leveled, no_key_dups(ObjSpecs) ]. - -mput(Pid, ObjSpecs) -> - leveled_bookie:book_mput(Pid, ObjSpecs). - -mput_next(S, _, [_Pid, ObjSpecs]) -> - ?CMD_VALID(S, mput, - lists:foldl(fun({add, Bucket, Key, _SubKey, Value}, #{model := Model, previous_keys := PK} = Acc) -> - Acc#{model => orddict:store({Bucket, Key}, {Value, []}, Model), - previous_keys => PK ++ [{Key, Bucket}]}; - ({remove, Bucket, Key, _SubKey, _Value}, #{model := Model} = Acc) -> - Acc#{model => orddict:erase({Bucket, Key}, Model)} - end, S, ObjSpecs), - S). - -mput_post(S, [_, _], Res) -> - ?CMD_VALID(S, mput, eq(Res, ok), eq(Res, {unsupported_message, mput})). - -mput_features(S, [_Pid, ObjSpecs], _Res) -> - ?CMD_VALID(S, mput, - {mput, [ element(1, ObjSpec) || ObjSpec <- ObjSpecs ]}, - [{mput, unsupported}]). - -%% --- Operation: head --- -head_pre(S) -> - is_leveled_open(S). - -head_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> - ?LET({Key, Bucket}, gen_key_in_bucket(PK), - [Pid, Bucket, Key, Tag]). - -head_pre(#{leveled := Leveled}, [Pid, _Bucket, _Key, _Tag]) -> - Pid == Leveled. - -head_adapt(#{leveled := Leveled}, [_, Bucket, Key, Tag]) -> - [Leveled, Bucket, Key, Tag]. - -head(Pid, Bucket, Key, none) -> - leveled_bookie:book_head(Pid, Bucket, Key); -head(Pid, Bucket, Key, Tag) -> - leveled_bookie:book_head(Pid, Bucket, Key, Tag). - -head_post(#{model := Model} = S, [_Pid, Bucket, Key, Tag], Res) -> - ?CMD_VALID(S, head, - case Res of - {ok, _MetaData} -> - orddict:find({Bucket, Key}, Model) =/= error; - not_found -> - %% Weird to be able to supply a tag, but must be STD_TAG... - implies(lists:member(maps:get(start_opts, S), [{head_only, with_lookup}]), - lists:member(Tag, [?STD_TAG, none, ?HEAD_TAG])) orelse - orddict:find({Bucket, Key}, Model) == error; - {unsupported_message, head} -> - Tag =/= ?HEAD_TAG - end, - eq(Res, {unsupported_message, head})). - -head_features(#{deleted_keys := DK, previous_keys := PK}, [_Pid, Bucket, Key, _Tag], Res) -> - case Res of - not_found -> - [{head, not_found, deleted} || lists:member({Key, Bucket}, DK)] ++ - [{head, not_found, not_inserted} || not lists:member({Key, Bucket}, PK)]; - {ok, {_, _, _}} -> %% Metadata - [{head, found}]; - {ok, Bin} when is_binary(Bin) -> - [{head, found_riak_object}]; - {unsupported_message, _} -> - [{head, unsupported}] - end. - - -%% --- Operation: delete --- -delete_pre(S) -> - is_leveled_open(S). - -delete_args(#{leveled := Pid, previous_keys := PK, tag := Tag}) -> - ?LET({Key, Bucket}, gen_key_in_bucket(PK), - [Pid, Bucket, Key, [], Tag]). - -delete_pre(#{leveled := Leveled, model := Model}, [Pid, Bucket, Key, Spec, _Tag]) -> - Pid == Leveled andalso - case orddict:find({Bucket, Key}, Model) of - error -> true; - {ok, {_, OldSpec}} -> - Spec == OldSpec - end. - -delete_adapt(#{leveled := Leveled, model := Model}, [_, Bucket, Key, Spec, Tag]) -> - NewSpec = - case orddict:find({Bucket, Key}, Model) of - error -> Spec; - {ok, {_, OldSpec}} -> - Spec == OldSpec - end, - [ Leveled, Bucket, Key, NewSpec, Tag ]. - -delete(Pid, Bucket, Key, Spec, ?STD_TAG) -> - leveled_bookie:book_delete(Pid, Bucket, Key, Spec); -delete(Pid, Bucket, Key, Spec, Tag) -> - leveled_bookie:book_put(Pid, Bucket, Key, delete, Spec, Tag). - -delete_next(#{model := Model, deleted_keys := DK} = S, _Value, [_Pid, Bucket, Key, _, _]) -> - ?CMD_VALID(S, delete, - S#{model => orddict:erase({Bucket, Key}, Model), - deleted_keys => DK ++ [{Key, Bucket} || orddict:is_key({Key, Bucket}, Model)]}, - S). - -delete_post(S, [_Pid, _Bucket, _Key, _, _], Res) -> - ?CMD_VALID(S, delete, - eq(Res, ok), - case Res of - {unsupported_message, _} -> true; - _ -> Res - end). - -delete_features(#{previous_keys := PK} = S, [_Pid, Bucket, Key, _, _], _Res) -> - ?CMD_VALID(S, delete, - case lists:member({Key, Bucket}, PK) of - true -> - [{delete, existing}]; - false -> - [{delete, none_existing}] - end, - [{delete, unsupported}]). - -%% --- Operation: is_empty --- -is_empty_pre(S) -> - is_leveled_open(S). - -is_empty_args(#{leveled := Pid, tag := Tag}) -> - [Pid, Tag]. - - -is_empty_pre(#{leveled := Leveled}, [Pid, _]) -> - Pid == Leveled. - -is_empty_adapt(#{leveled := Leveled}, [_, Tag]) -> - [Leveled, Tag]. - -%% @doc is_empty - The actual operation -is_empty(Pid, Tag) -> - leveled_bookie:book_isempty(Pid, Tag). - -is_empty_post(#{model := Model}, [_Pid, _Tag], Res) -> - Size = orddict:size(Model), - case Res of - true -> eq(0, Size); - false when Size == 0 -> expected_empty; - false when Size > 0 -> true - end. - -is_empty_features(_S, [_Pid, _], Res) -> - [{empty, Res}]. - -%% --- Operation: drop --- -drop_pre(S) -> - is_leveled_open(S). - -drop_args(#{leveled := Pid, dir := Dir} = S) -> - ?LET([Tag, _, Name], init_backend_args(S), - [Pid, Tag, [{root_path, Dir} | gen_opts()], Name]). - -drop_pre(#{leveled := Leveled} = S, [Pid, Tag, Opts, Name]) -> - Pid == Leveled andalso init_backend_pre(S, [Tag, Opts, Name]). - -drop_adapt(#{leveled := Leveled} = S, [_Pid, Tag, Opts, Name]) -> - [Leveled | init_backend_adapt(S, [Tag, Opts, Name])]. - -%% @doc drop - The actual operation -%% Remove fles from disk (directory structure may remain) and start a new clean database -drop(Pid, Tag, Opts, Name) -> - Mon = erlang:monitor(process, Pid), - ok = leveled_bookie:book_destroy(Pid), - receive - {'DOWN', Mon, _Type, Pid, _Info} -> - init_backend(Tag, Opts, Name) - after 5000 -> - {still_alive, Pid, Name} - end. - -drop_next(S, Value, [Pid, Tag, Opts, Name]) -> - S1 = stop_next(S, Value, [Pid]), - init_backend_next(S1#{model => orddict:new()}, - Value, [Tag, Opts, Name]). - -drop_post(_S, [_Pid, _Tag, _Opts, _], Res) -> - case is_pid(Res) of - true -> true; - false -> Res - end. - -drop_features(#{model := Model}, [_Pid, _Tag, _Opts, _], _Res) -> - Size = orddict:size(Model), - [{drop, empty} || Size == 0 ] ++ - [{drop, small} || Size > 0 andalso Size < 20 ] ++ - [{drop, medium} || Size >= 20 andalso Size < 1000 ] ++ - [{drop, large} || Size >= 1000 ]. - - - -%% --- Operation: kill --- -%% Test that killing the root Pid of leveled has the same effect as closing it nicely -%% that means, we don't loose data! Not even when parallel successful puts are going on. -kill_pre(S) -> - is_leveled_open(S). - -kill_args(#{leveled := Pid}) -> - [Pid]. - -kill_pre(#{leveled := Leveled}, [Pid]) -> - Pid == Leveled. - -kill_adapt(#{leveled := Leveled}, [_]) -> - [ Leveled ]. - -kill(Pid) -> - exit(Pid, kill), - timer:sleep(1). - -kill_next(S, Value, [Pid]) -> - stop_next(S, Value, [Pid]). - - - - -%% --- Operation: compacthappened --- - -compacthappened(DataDir) -> - PostCompact = filename:join(DataDir, "journal/journal_files/post_compact"), - case filelib:is_dir(PostCompact) of - true -> - {ok, Files} = file:list_dir(PostCompact), - Files; - false -> - [] - end. - -ledgerpersisted(DataDir) -> - LedgerPath = filename:join(DataDir, "ledger/ledger_files"), - case filelib:is_dir(LedgerPath) of - true -> - {ok, Files} = file:list_dir(LedgerPath), - Files; - false -> - [] - end. - -journalwritten(DataDir) -> - JournalPath = filename:join(DataDir, "journal/journal_files"), - case filelib:is_dir(JournalPath) of - true -> - {ok, Files} = file:list_dir(JournalPath), - Files; - false -> - [] - end. - - -%% --- Operation: compact journal --- - -compact_pre(S) -> - is_leveled_open(S). - -compact_args(#{leveled := Pid}) -> - [Pid, nat()]. - -compact_pre(#{leveled := Leveled}, [Pid, _TS]) -> - Pid == Leveled. - -compact_adapt(#{leveled := Leveled}, [_Pid, TS]) -> - [ Leveled, TS ]. - -compact(Pid, TS) -> - leveled_bookie:book_compactjournal(Pid, TS). - -compact_next(S, R, [_Pid, _TS]) -> - case {R, maps:get(previous_compact, S, undefined)} of - {ok, undefined} -> - S#{previous_compact => true}; - _ -> - S - end. - -compact_post(S, [_Pid, _TS], Res) -> - case Res of - ok -> - true; - busy -> - true == maps:get(previous_compact, S, undefined) - end. - -compact_features(S, [_Pid, _TS], _Res) -> - case maps:get(previous_compact, S, undefined) of - undefined -> - [{compact, fresh}]; - _ -> - [{compact, repeat}] - end. - - -%% Testing fold: -%% Note async and sync mode! -%% see https://github.com/martinsumner/riak_kv/blob/mas-2.2.5-tictactaae/src/riak_kv_leveled_backend.erl#L238-L419 - -%% --- Operation: index folding --- -indexfold_pre(S) -> - is_leveled_open(S). - -indexfold_args(#{leveled := Pid, counter := Counter, previous_keys := PK}) -> - ?LET({Key, Bucket}, gen_key_in_bucket(PK), - [Pid, default(Bucket, {Bucket, Key}), gen_foldacc(3), - ?LET({[N], M}, {gen_index_value(), choose(0,2)}, {gen_category(), [N], [N+M]}), - {bool(), - oneof([undefined, gen_index_value()])}, - Counter %% add a unique counter - ]). - -indexfold_pre(#{leveled := Leveled}, [Pid, _Constraint, _FoldAccT, _Range, _TermHandling, _Counter]) -> - %% Make sure we operate on an existing Pid when shrinking - %% Check start options validity as well? - Pid == Leveled. - -indexfold_adapt(#{leveled := Leveled}, [_, Constraint, FoldAccT, Range, TermHandling, Counter]) -> - %% Keep the counter! - [Leveled, Constraint, FoldAccT, Range, TermHandling, Counter]. - -indexfold(Pid, Constraint, FoldAccT, Range, {_, undefined} = TermHandling, _Counter) -> - {async, Folder} = leveled_bookie:book_indexfold(Pid, Constraint, FoldAccT, Range, TermHandling), - Folder; -indexfold(Pid, Constraint, FoldAccT, Range, {ReturnTerms, RegExp}, _Counter) -> - {ok, RE} = re:compile(RegExp), - {async, Folder} = leveled_bookie:book_indexfold(Pid, Constraint, FoldAccT, Range, {ReturnTerms, RE}), - Folder. - -indexfold_next(#{folders := Folders} = S, SymFolder, - [_, Constraint, {Fun, Acc}, {Category, From, To}, {ReturnTerms, RegExp}, Counter]) -> - ConstraintFun = - fun(B, K, Bool) -> - case Constraint of - {B, KStart} -> not Bool orelse K >= KStart; - B -> true; - _ -> false - end - end, - S#{folders => - Folders ++ - [#{counter => Counter, - type => indexfold, - folder => SymFolder, - reusable => true, - result => fun(Model) -> - Select = - lists:sort( - orddict:fold(fun({B, K}, {_V, Spec}, A) -> - [ {B, {Idx, K}} - || {Cat, Idx} <- Spec, - Idx >= From, Idx =< To, - Cat == Category, - ConstraintFun(B, K, Idx == From), - RegExp == undefined orelse string:find(Idx, RegExp) =/= nomatch - ] ++ A - end, [], Model)), - lists:foldl(fun({B, NK}, A) when ReturnTerms -> - Fun(B, NK, A); - ({B, {_, NK}}, A) -> - Fun(B, NK, A) - end, Acc, Select) - end - }], - counter => Counter + 1}. - -indexfold_post(_S, _, Res) -> - is_function(Res). - -indexfold_features(_S, [_Pid, Constraint, FoldAccT, _Range, {ReturnTerms, _}, _Counter], _Res) -> - [{foldAccT, FoldAccT}] ++ %% This will be extracted for printing later - [{index_fold, bucket} || not is_tuple(Constraint) ] ++ - [{index_fold, bucket_and_primary_key} || is_tuple(Constraint)] ++ - [{index_fold, return_terms, ReturnTerms} ]. - - - - -%% --- Operation: keylist folding --- -%% slack discussion: "`book_keylist` only passes `Bucket` and `Key` into the accumulator, ignoring SubKey - -%% so I don't think this can be used in head_only mode to return results that make sense" -%% -%% There are also keylist functions that take a specific bucket and range into account. Not considered yet. -keylistfold_pre(S) -> - is_leveled_open(S). - -keylistfold_args(#{leveled := Pid, counter := Counter, tag := Tag}) -> - [Pid, Tag, gen_foldacc(3), - Counter %% add a unique counter - ]. - -keylistfold_pre(#{leveled := Leveled}, [Pid, _Tag, _FoldAccT, _Counter]) -> - %% Make sure we operate on an existing Pid when shrinking - %% Check start options validity as well? - Pid == Leveled. - -keylistfold_adapt(#{leveled := Leveled}, [_, Tag, FoldAccT, Counter]) -> - %% Keep the counter! - [Leveled, Tag, FoldAccT, Counter]. - -keylistfold(Pid, Tag, FoldAccT, _Counter) -> - {async, Folder} = leveled_bookie:book_keylist(Pid, Tag, FoldAccT), - Folder. - -keylistfold_next(#{folders := Folders, model := Model} = S, SymFolder, - [_, _Tag, {Fun, Acc}, Counter]) -> - S#{folders => - Folders ++ - [#{counter => Counter, - type => keylist, - folder => SymFolder, - reusable => false, - result => fun(_) -> orddict:fold(fun({B, K}, _V, A) -> Fun(B, K, A) end, Acc, Model) end - }], - counter => Counter + 1}. - -keylistfold_post(_S, _, Res) -> - is_function(Res). - -keylistfold_features(_S, [_Pid, _Tag, FoldAccT, _Counter], _Res) -> - [{foldAccT, FoldAccT}]. %% This will be extracted for printing later - - -%% --- Operation: bucketlistfold --- -bucketlistfold_pre(S) -> - is_leveled_open(S). - -bucketlistfold_args(#{leveled := Pid, counter := Counter, tag := Tag}) -> - [Pid, Tag, gen_foldacc(2), elements([first, all]), Counter]. - -bucketlistfold_pre(#{leveled := Leveled}, [Pid, _Tag, _FoldAccT, _Constraints, _]) -> - Pid == Leveled. - -bucketlistfold_adapt(#{leveled := Leveled}, [_Pid, Tag, FoldAccT, Constraints, Counter]) -> - [Leveled, Tag, FoldAccT, Constraints, Counter]. - -bucketlistfold(Pid, Tag, FoldAccT, Constraints, _) -> - {async, Folder} = leveled_bookie:book_bucketlist(Pid, Tag, FoldAccT, Constraints), - Folder. - -bucketlistfold_next(#{folders := Folders} = S, SymFolder, - [_, _, {Fun, Acc}, Constraints, Counter]) -> - S#{folders => - Folders ++ - [#{counter => Counter, - type => bucketlist, - folder => SymFolder, - reusable => true, - result => fun(Model) -> - Bs = orddict:fold(fun({B, _K}, _V, A) -> A ++ [B || not lists:member(B, A)] end, [], Model), - case {Constraints, Bs} of - {all, _} -> - lists:foldl(fun(B, A) -> Fun(B, A) end, Acc, Bs); - {first, []} -> - Acc; - {first, [First|_]} -> - lists:foldl(fun(B, A) -> Fun(B, A) end, Acc, [First]) - end - end - }], - counter => Counter + 1}. - -bucketlistfold_post(_S, [_Pid, _Tag, _FoldAccT, _Constraints, _], Res) -> - is_function(Res). - -bucketlistfold_features(_S, [_Pid, _Tag, FoldAccT, _Constraints, _], _Res) -> - [ {foldAccT, FoldAccT} ]. - -%% --- Operation: objectfold --- -objectfold_pre(S) -> - is_leveled_open(S). - -objectfold_args(#{leveled := Pid, counter := Counter, tag := Tag}) -> - [Pid, Tag, gen_foldacc(4), bool(), Counter]. - -objectfold_pre(#{leveled := Leveled}, [Pid, _Tag, _FoldAccT, _Snapshot, _Counter]) -> - Leveled == Pid. - -objectfold_adapt(#{leveled := Leveled}, [_Pid, Tag, FoldAccT, Snapshot, Counter]) -> - [Leveled, Tag, FoldAccT, Snapshot, Counter]. - -objectfold(Pid, Tag, FoldAccT, Snapshot, _Counter) -> - {async, Folder} = leveled_bookie:book_objectfold(Pid, Tag, FoldAccT, Snapshot), - Folder. - -objectfold_next(#{folders := Folders, model := Model} = S, SymFolder, - [_Pid, _Tag, {Fun, Acc}, Snapshot, Counter]) -> - S#{folders => - Folders ++ - [#{counter => Counter, - type => objectfold, - folder => SymFolder, - reusable => not Snapshot, - result => fun(M) -> - OnModel = - case Snapshot of - true -> Model; - false -> M - end, - Objs = orddict:fold(fun({B, K}, {V, _}, A) -> [{B, K, V} | A] end, [], OnModel), - lists:foldr(fun({B, K, V}, A) -> Fun(B, K, V, A) end, Acc, Objs) - end - }], - counter => Counter + 1}. - -objectfold_post(_S, [_Pid, _Tag, _FoldAccT, _Snapshot, _Counter], Res) -> - is_function(Res). - -objectfold_features(_S, [_Pid, _Tag, FoldAccT, _Snapshot, _Counter], _Res) -> - [{foldAccT, FoldAccT}]. %% This will be extracted for printing later - - - - -%% --- Operation: fold_run --- -fold_run_pre(S) -> - maps:get(folders, S, []) =/= []. - -fold_run_args(#{folders := Folders}) -> - ?LET(#{counter := Counter, folder := Folder}, elements(Folders), - [Counter, Folder]). - -fold_run_pre(#{folders := Folders}, [Counter, _Folder]) -> - %% Ensure membership even under shrinking - %% Counter is fixed at first generation and does not shrink! - get_foldobj(Folders, Counter) =/= undefined. - -fold_run(_, Folder) -> - catch Folder(). - -fold_run_next(#{folders := Folders} = S, _Value, [Counter, _Folder]) -> - %% leveled_runner comment: "Iterators should de-register themselves from the Penciller on completion." - FoldObj = get_foldobj(Folders, Counter), - case FoldObj of - #{reusable := false} -> - UsedFolders = maps:get(used_folders, S, []), - S#{folders => Folders -- [FoldObj], - used_folders => UsedFolders ++ [FoldObj]}; - _ -> - S - end. - -fold_run_post(#{folders := Folders, leveled := Leveled, model := Model}, [Count, _], Res) -> - case Leveled of - undefined -> - is_exit(Res); - _ -> - #{result := ResFun} = get_foldobj(Folders, Count), - eq(Res, ResFun(Model)) - end. - -fold_run_features(#{folders := Folders, leveled := Leveled}, [Count, _Folder], Res) -> - #{type := Type} = get_foldobj(Folders, Count), - [ {fold_run, Type} || Leveled =/= undefined ] ++ - [ fold_run_on_stopped_leveled || Leveled == undefined ] ++ - [ {fold_run, found_list, length(Res)}|| is_list(Res) ] ++ - [ {fold_run, found_integer}|| is_integer(Res) ]. - - -%% --- Operation: fold_run on already used folder --- -%% A fold that has already ran to completion should results in an exception when re-used. -%% leveled_runner comment: "Iterators should de-register themselves from the Penciller on completion." -noreuse_fold_pre(S) -> - maps:get(used_folders, S, []) =/= []. - -noreuse_fold_args(#{used_folders := Folders}) -> - ?LET(#{counter := Counter, folder := Folder}, elements(Folders), - [Counter, Folder]). - -noreuse_fold_pre(S, [Counter, _Folder]) -> - %% Ensure membership even under shrinking - %% Counter is fixed at first generation and does not shrink! - lists:member(Counter, - [ maps:get(counter, Used) || Used <- maps:get(used_folders, S, []) ]). - -noreuse_fold(_, Folder) -> - catch Folder(). - -noreuse_fold_post(_S, [_, _], Res) -> - is_exit(Res). - -noreuse_fold_features(_, [_, _], _) -> - [ reuse_fold ]. - - -%% --- Operation: fold_run on folder that survived a crash --- -%% A fold that has already ran to completion should results in an exception when re-used. -stop_fold_pre(S) -> - maps:get(stop_folders, S, []) =/= []. - -stop_fold_args(#{stop_folders := Folders}) -> - ?LET(#{counter := Counter, folder := Folder}, elements(Folders), - [Counter, Folder]). - -stop_fold_pre(S, [Counter, _Folder]) -> - %% Ensure membership even under shrinking - %% Counter is fixed at first generation and does not shrink! - lists:member(Counter, - [ maps:get(counter, Used) || Used <- maps:get(stop_folders, S, []) ]). - -stop_fold(_, Folder) -> - catch Folder(). - -stop_fold_post(_S, [_Counter, _], Res) -> - is_exit(Res). - -stop_fold_features(S, [_, _], _) -> - [ case maps:get(leveled, S) of - undefined -> - stop_fold_when_closed; - _ -> - stop_fold_when_open - end ]. - - -weight(#{previous_keys := []}, get) -> - 1; -weight(#{previous_keys := []}, delete) -> - 1; -weight(S, C) when C == get; - C == put; - C == delete; - C == updateload -> - ?CMD_VALID(S, put, 10, 1); -weight(_S, stop) -> - 1; -weight(_, _) -> - 1. - - -is_valid_cmd(S, put) -> - not in_head_only_mode(S); -is_valid_cmd(S, delete) -> - is_valid_cmd(S, put); -is_valid_cmd(S, get) -> - not in_head_only_mode(S); -is_valid_cmd(S, head) -> - not lists:member({head_only, no_lookup}, maps:get(start_opts, S, [])); -is_valid_cmd(S, mput) -> - in_head_only_mode(S). - - - -%% @doc check that the implementation of leveled is equivalent to a -%% sorted dict at least --spec prop_db() -> eqc:property(). -prop_db() -> - Dir = "./leveled_data", - eqc:dont_print_counterexample( - ?LET(Shrinking, parameter(shrinking, false), - ?FORALL({Kind, Cmds}, oneof([{seq, more_commands(20, commands(?MODULE))}, - {par, more_commands(2, parallel_commands(?MODULE))} - ]), - begin - delete_level_data(Dir), - ?IMPLIES(empty_dir(Dir), - ?ALWAYS(if Shrinking -> 10; true -> 1 end, - begin - Procs = erlang:processes(), - StartTime = erlang:system_time(millisecond), - - RunResult = execute(Kind, Cmds, [{dir, Dir}]), - %% Do not extract the 'state' from this tuple, since parallel commands - %% miss the notion of final state. - CallFeatures = [ Feature || Feature <- call_features(history(RunResult)), - not is_foldaccT(Feature), - not (is_tuple(Feature) andalso element(1, Feature) == start_options) - ], - StartOptionFeatures = [ lists:keydelete(root_path, 1, Feature) || {start_options, Feature} <- call_features(history(RunResult)) ], - - timer:sleep(1000), - CompactionFiles = compacthappened(Dir), - LedgerFiles = ledgerpersisted(Dir), - JournalFiles = journalwritten(Dir), - % io:format("File counts: Compacted ~w Journal ~w Ledger ~w~n", [length(CompactionFiles), length(LedgerFiles), length(JournalFiles)]), - - - - case whereis(maps:get(sut, initial_state())) of - undefined -> - % io:format("Init state undefined - deleting~n"), - delete_level_data(Dir); - Pid when is_pid(Pid) -> - % io:format("Init state defined - destroying~n"), - leveled_bookie:book_destroy(Pid) - end, - - Wait = wait_for_procs(Procs, 1500), - RunTime = erlang:system_time(millisecond) - StartTime, - - %% Since in parallel commands we don't have access to the state, we retrieve functions - %% from the features - FoldAccTs = [ FoldAccT || Entry <- history(RunResult), - {foldAccT, FoldAccT} <- eqc_statem:history_features(Entry)], - - pretty_commands(?MODULE, Cmds, RunResult, - measure(time_per_test, RunTime, - aggregate(command_names(Cmds), - collect(Kind, - measure(compaction_files, length(CompactionFiles), - measure(ledger_files, length(LedgerFiles), - measure(journal_files, length(JournalFiles), - aggregate(with_title('Features'), CallFeatures, - aggregate(with_title('Start Options'), StartOptionFeatures, - features(CallFeatures, - conjunction([{result, - ?WHENFAIL([ begin - eqc:format("~p with acc ~p:\n~s\n", [F, Acc, - show_function(F)]) - end || {F, Acc} <- FoldAccTs ], - result(RunResult) == ok)}, - {data_cleanup, - ?WHENFAIL(eqc:format("~s\n", [os:cmd("ls -Rl " ++ Dir)]), - empty_dir(Dir))}, - {pid_cleanup, equals(Wait, [])}]))))))))))) - end)) - end))). - -history({H, _, _}) -> H. -result({_, _, Res}) -> Res. - -execute(seq, Cmds, Env) -> - run_commands(Cmds, Env); -execute(par, Cmds, Env) -> - run_parallel_commands(Cmds, Env). - -is_exit({'EXIT', _}) -> - true; -is_exit(Other) -> - {expected_exit, Other}. - -is_foldaccT({foldAccT, _}) -> - true; -is_foldaccT(_) -> - false. - -show_function(F) -> - case proplists:get_value(module, erlang:fun_info(F)) of - eqc_fun -> - eqc_fun:show_function(F); - _ -> - proplists:get_value(name, erlang:fun_info(F)) - end. - - -%% slack discussion: -%% `max_journalsize` should be at least 2048 + byte_size(smallest_object) + byte_size(smallest_object's key) + overhead (which is a few bytes per K/V pair). -gen_opts() -> - options([%% {head_only, elements([false, no_lookup, with_lookup])} we don't test head_only mode - {compression_method, elements([native, lz4])} - , {compression_point, elements([on_compact, on_receipt])} - %% , {max_journalsize, ?LET(N, nat(), 2048 + 1000 + 32 + 16 + 16 + N)} - %% , {cache_size, oneof([nat(), 2048, 2060, 5000])} - ]). - -options(GenList) -> - ?LET(Bools, vector(length(GenList), bool()), - [ Opt || {Opt, true} <- lists:zip(GenList, Bools)]). - - -gen_key() -> - binary(16). - -%% Cannot be atoms! -%% key() type specified: should be binary(). -gen_bucket() -> - elements([<<"bucket1">>, <<"bucket2">>, <<"bucket3">>]). - -gen_val() -> - noshrink(binary(256)). - -gen_categories(?RIAK_TAG) -> - sublist(categories()); -gen_categories(_) -> - %% for ?STD_TAG this seems to make little sense - []. - - -categories() -> - [dep, lib]. - -gen_category() -> - elements(categories()). - -gen_index_value() -> - %% Carefully selected to have one out-layer and several border cases. - [elements("arts")]. - -gen_key_in_bucket([]) -> - {gen_key(), gen_bucket()}; -gen_key_in_bucket(Previous) -> - ?LET({K, B}, elements(Previous), - frequency([{1, gen_key_in_bucket([])}, - {1, {K, gen_bucket()}}, - {2, {K, B}}])). - -gen_foldacc(2) -> - ?SHRINK(oneof([{eqc_fun:function2(int()), int()}, - {eqc_fun:function2(list(int())), list(int())}]), - [fold_buckets()]); -gen_foldacc(3) -> - ?SHRINK(oneof([{eqc_fun:function3(int()), int()}, - {eqc_fun:function3(list(int())), list(int())}]), - [fold_collect()]); -gen_foldacc(4) -> - ?SHRINK(oneof([{eqc_fun:function4(int()), int()}, - {eqc_fun:function4(list(int())), list(int())}]), - [fold_objects()]). - - - -fold_buckets() -> - {fun(B, Acc) -> [B | Acc] end, []}. - -fold_collect() -> - {fun(X, Y, Acc) -> [{X, Y} | Acc] end, []}. - -fold_objects() -> - {fun(X, Y, Z, Acc) -> [{X, Y, Z} | Acc] end, []}. - -%% This makes system fall over -fold_collect_no_acc() -> - fun(X, Y, Z) -> [{X, Y} | Z] end. - -fold_count() -> - {fun(_X, _Y, Z) -> Z + 1 end, 0}. - -fold_keys() -> - {fun(X, _Y, Z) -> [X | Z] end, []}. - - -empty_dir(Dir) -> - case file:list_dir(Dir) of - {error, enoent} -> true; - {ok, Ds} -> - lists:all(fun(D) -> empty_dir(filename:join(Dir, D)) end, Ds); - _ -> - false - end. - -get_foldobj([], _Counter) -> - undefined; -get_foldobj([#{counter := Counter} = Map | _Rest], Counter) -> - Map; -get_foldobj([_ | Rest], Counter) -> - get_foldobj(Rest, Counter). - - -%% Helper for all those preconditions that just check that leveled Pid -%% is populated in state. (We cannot check with is_pid, since that's -%% symbolic in test case generation!). -is_leveled_open(S) -> - maps:get(leveled, S, undefined) =/= undefined. - -in_head_only_mode(S) -> - proplists:get_value(head_only, maps:get(start_opts, S, []), false) =/= false. - -wait_for_procs(Known, Timeout) -> - case erlang:processes() -- Known of - [] -> []; - _NonEmptyList -> - if - Timeout > 0 -> - timer:sleep(100), - wait_for_procs(Known, Timeout - 100); - true -> - timer:sleep(10000), - erlang:processes() -- Known - end - end. - -delete_level_data(Dir) -> - os:cmd("rm -rf " ++ Dir). - -%% Slack discussion: -%% `[{add, B1, K1, SK1}, {add, B1, K1, SK2}]` should be fine (same bucket and key, different subkey) -no_key_dups([]) -> - []; -no_key_dups([{_Action, Bucket, Key, SubKey, _Value} = E | Es]) -> - [E | no_key_dups([ {A, B, K, SK, V} || {A, B, K, SK, V} <- Es, - {B, K, SK} =/= {Bucket, Key, SubKey}])]. \ No newline at end of file From e3bd83179a656726e774ded3bf9863d8eaad47a0 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Sun, 27 Jan 2019 23:31:44 +0000 Subject: [PATCH 15/15] Uncomment tests! --- test/end_to_end/basic_SUITE.erl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/end_to_end/basic_SUITE.erl b/test/end_to_end/basic_SUITE.erl index 12cda7e..a59da44 100644 --- a/test/end_to_end/basic_SUITE.erl +++ b/test/end_to_end/basic_SUITE.erl @@ -18,16 +18,16 @@ ]). all() -> [ - % simple_put_fetch_head_delete, - % many_put_fetch_head, - % journal_compaction, - % fetchput_snapshot, - % load_and_count, - % load_and_count_withdelete, - % space_clear_ondelete, - % is_empty_test, - % many_put_fetch_switchcompression, - % bigjournal_littlejournal, + simple_put_fetch_head_delete, + many_put_fetch_head, + journal_compaction, + fetchput_snapshot, + load_and_count, + load_and_count_withdelete, + space_clear_ondelete, + is_empty_test, + many_put_fetch_switchcompression, + bigjournal_littlejournal, bigsst_littlesst, safereaderror_startup, remove_journal_test