%% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at https://mozilla.org/MPL/2.0/. %% %% Copyright (c) 2017-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. %% @hidden -module(ra_mt). -include("ra.hrl"). -export([ init/1, init/2, init_successor/3, insert/2, insert_sparse/3, stage/2, commit/1, abort/1, lookup/2, lookup_term/2, tid_for/3, fold/5, fold/6, get_items/2, record_flushed/3, set_first/2, delete/1, tid/1, staged/1, is_active/2, prev/1, indexes/1, info/1, range/1, range_overlap/2 ]). -define(MAX_MEMTBL_ENTRIES, 1_000_000). -define(IS_NEXT_IDX(Idx, Seq), (Seq == [] orelse (is_integer(hd(Seq)) andalso hd(Seq) + 1 == Idx) orelse (Idx == element(2, hd(Seq)) + 1))). -record(?MODULE, {tid :: ets:tid(), indexes = [] :: ra_seq:state(), size = 0 :: non_neg_integer(), staged :: undefined | {NumStaged :: non_neg_integer(), [log_entry()]}, prev :: undefined | #?MODULE{} }). -opaque state() :: #?MODULE{}. -type delete_spec() :: undefined | {'<', ets:tid(), ra:index()} | {delete, ets:tid()} | {indexes, ets:tid(), ra_seq:state()} | {multi, [delete_spec()]}. -export_type([ state/0, delete_spec/0 ]). -spec init(ets:tid(), read | read_write) -> state(). init(Tid, Mode) -> Seq = case Mode of read -> []; read_write -> %% Use ets:select for efficient projection - extracts only indexes %% without building intermediate tuples or function closures ra_seq:from_list( ets:select(Tid, [{{'$1', '_', '_'}, [], ['$1']}])) end, #?MODULE{tid = Tid, indexes = Seq, size = ets:info(Tid, size)}. -spec init(ets:tid()) -> state(). init(Tid) -> init(Tid, read_write). -spec init_successor(ets:tid(), read | read_write, state()) -> state(). init_successor(Tid, Mode, #?MODULE{} = State) -> Succ = init(Tid, Mode), Succ#?MODULE{prev = State}. -spec insert(log_entry(), state()) -> {ok, state()} | {error, overwriting | limit_reached}. insert({Idx, _, _} = Entry, #?MODULE{tid = Tid, indexes = Seq, size = Size} = State) when ?IS_NEXT_IDX(Idx, Seq) -> case Size > ?MAX_MEMTBL_ENTRIES of true -> {error, limit_reached}; false -> true = ets:insert(Tid, Entry), {ok, State#?MODULE{indexes = update_ra_seq(Idx, Seq), size = Size + 1}} end; insert({Idx, _, _} = _Entry, #?MODULE{indexes = Seq}) -> case Idx =< ra_seq:last(Seq) of true -> {error, overwriting}; false -> exit({unexpected_sparse_insert, Idx, Seq}) end. -spec insert_sparse(log_entry(), undefined | ra:index(), state()) -> {ok, state()} | {error, overwriting | gap_detected | limit_reached}. insert_sparse({Idx, _, _} = Entry, _LastIdx, #?MODULE{tid = Tid, indexes = []} = State) -> %% when the indexes is empty always accept the next entry true = ets:insert(Tid, Entry), {ok, State#?MODULE{indexes = ra_seq:append(Idx, []), size = 1}}; insert_sparse({Idx, _, _} = Entry, LastIdx, #?MODULE{tid = Tid, indexes = Seq, size = Size} = State) -> LastSeq = ra_seq:last(Seq), IsOverwriting = Idx =< LastSeq andalso is_integer(LastSeq), case LastSeq == LastIdx andalso not IsOverwriting of true -> case Size > ?MAX_MEMTBL_ENTRIES of true -> {error, limit_reached}; false -> true = ets:insert(Tid, Entry), {ok, State#?MODULE{indexes = ra_seq:append(Idx, Seq), size = Size + 1}} end; false -> case IsOverwriting of true -> {error, overwriting}; false -> {error, gap_detected} end end. -spec stage(log_entry(), state()) -> {ok, state()} | {error, overwriting | limit_reached}. stage({Idx, _, _} = Entry, #?MODULE{staged = {FstIdx, Staged}, indexes = Range, size = Size} = State) when ?IS_NEXT_IDX(Idx, Range) -> {ok, State#?MODULE{staged = {FstIdx, [Entry | Staged]}, indexes = update_ra_seq(Idx, Range), size = Size + 1}}; stage({Idx, _, _} = Entry, #?MODULE{tid = _Tid, staged = undefined, indexes = Seq, size = Size} = State) when ?IS_NEXT_IDX(Idx, Seq) -> case Size > ?MAX_MEMTBL_ENTRIES of true -> %% the limit cannot be reached during transaction {error, limit_reached}; false -> {ok, State#?MODULE{staged = {Idx, [Entry]}, indexes = update_ra_seq(Idx, Seq), size = Size + 1}} end; stage({Idx, _, _} = _Entry, #?MODULE{indexes = Seq}) -> case Idx =< ra_seq:last(Seq) of true -> {error, overwriting}; false -> exit({unexpected_sparse_stage, Idx, Seq}) end. -spec staged(state()) -> [log_entry()]. staged(#?MODULE{staged = undefined}) -> []; staged(#?MODULE{staged = {_, Staged0}, prev = Prev0}) -> PrevStaged = case Prev0 of undefined -> []; _ -> staged(Prev0) end, PrevStaged ++ lists:reverse(Staged0). -spec commit(state()) -> {[log_entry()], state()}. commit(#?MODULE{staged = undefined} = State) -> {[], State}; commit(#?MODULE{tid = Tid, staged = {_, Staged0}, prev = Prev0} = State) -> {PrevStaged, Prev} = case Prev0 of undefined -> {[], Prev0}; _ -> commit(Prev0) end, Staged = lists:reverse(Staged0), true = ets:insert(Tid, Staged), %% TODO: mt: could prev contain overwritten entries? {PrevStaged ++ Staged, State#?MODULE{staged = undefined, prev = Prev}}. -spec abort(state()) -> state(). abort(#?MODULE{staged = undefined} = State) -> State; abort(#?MODULE{indexes = Seq, staged = {_, Staged0}} = State) -> {Idx, _, _} = lists:last(Staged0), State#?MODULE{staged = undefined, indexes = ra_seq:limit(Idx - 1, Seq)}. -spec lookup(ra:index(), state()) -> log_entry() | undefined. lookup(Idx, #?MODULE{staged = {FstStagedIdx, Staged}}) when Idx >= FstStagedIdx -> %% staged read case lists:keysearch(Idx, 1, Staged) of {value, Entry} -> Entry; _ -> undefined end; lookup(Idx, #?MODULE{tid = Tid, indexes = Seq, prev = Prev, staged = undefined}) -> %% ra_seq:in/2 could be expensive for sparse mem tables, %% TODO: consider checking ets table first case ra_seq:in(Idx, Seq) of true -> [Entry] = ets:lookup(Tid, Idx), Entry; false when Prev == undefined-> undefined; false -> lookup(Idx, Prev) end. -spec lookup_term(ra:index(), state()) -> ra_term() | undefined. lookup_term(Idx, #?MODULE{staged = {FstStagedIdx, Staged}}) when Idx >= FstStagedIdx -> %% staged read case lists:keysearch(Idx, 1, Staged) of {value, {_, T, _}} -> T; _ -> undefined end; lookup_term(Idx, #?MODULE{tid = Tid, prev = Prev, indexes = _Seq}) -> %% Note: This bypasses Seq check for efficiency. The ETS lookup handles %% the common case; Seq validation could be added if needed for correctness. case ets:lookup_element(Tid, Idx, 2, undefined) of undefined when Prev =/= undefined -> lookup_term(Idx, Prev); Term -> Term end. -spec tid_for(ra:index(), ra_term(), state()) -> undefined | ets:tid(). tid_for(_Idx, _Term, undefined) -> undefined; tid_for(Idx, Term, State) -> Tid = tid(State), case ets:lookup_element(Tid, Idx, 2, undefined) of Term -> Tid; _ -> tid_for(Idx, Term, State#?MODULE.prev) end. -spec fold(ra:index(), ra:index(), fun(), term(), state(), MissingKeyStrategy :: error | return) -> term(). fold(From, To, Fun, Acc, State, MissingKeyStrat) when is_atom(MissingKeyStrat) andalso To >= From -> case lookup(From, State) of undefined when MissingKeyStrat == error -> error({missing_key, From, Acc}); undefined when MissingKeyStrat == return -> Acc; E -> fold(From + 1, To, Fun, Fun(E, Acc), State, MissingKeyStrat) end; fold(_From, _To, _Fun, Acc, _State, _Strat) -> Acc. -spec fold(ra:index(), ra:index(), fun(), term(), state()) -> term(). fold(From, To, Fun, Acc, State) -> fold(From, To, Fun, Acc, State, error). -spec get_items([ra:index()], state()) -> {[log_entry()], NumRead :: non_neg_integer(), Remaining :: [ra:index()]}. get_items(Indexes, #?MODULE{} = State) -> read_sparse(Indexes, State, []). -spec delete(delete_spec()) -> non_neg_integer(). delete(undefined) -> 0; delete({indexes, _Tid, []}) -> 0; delete({indexes, Tid, Seq}) -> NumToDelete = ra_seq:length(Seq), Start = ra_seq:first(Seq), End = ra_seq:last(Seq), Limit = ets:info(Tid, size) div 2, %% check if there is an entry below the start of the deletion range, %% if there is we've missed a segment event at some point and need %% to perform a mop-up delete with `<`, irrespective of how many entries LowerExists = ets:member(Tid, Start-1), case NumToDelete > Limit orelse LowerExists of true -> %% more than half the table is to be deleted delete({'<', Tid, End + 1}); false -> _ = ra_seq:fold(fun (I, Acc) -> _ = ets:delete(Tid, I), Acc end, undefined, Seq), NumToDelete end; delete({Op, Tid, Idx}) when is_integer(Idx) andalso is_atom(Op) -> DelSpec = [{{'$1', '_', '_'}, [{'<', '$1', Idx}], [true]}], ets:select_delete(Tid, DelSpec); delete({delete, Tid}) -> Sz = ets:info(Tid, size), true = ets:delete(Tid), Sz; delete({multi, Specs}) -> lists:foldl( fun (Spec, Acc) -> try delete(Spec) of Sz -> Acc + Sz catch _:badarg -> %% table probably didn't exist Acc end end, 0, Specs). -spec range_overlap(ra:range(), state()) -> {Overlap :: ra:range(), Remainder :: ra:range()}. range_overlap(ReqRange, #?MODULE{} = State) -> Range = range(State), case ra_range:overlap(ReqRange, Range) of undefined -> {undefined, ReqRange}; Overlap -> {Overlap, case ra_range:subtract(Overlap, ReqRange) of [] -> undefined; [R] -> R end} end. -spec range(state()) -> undefined | {ra:index(), ra:index()}. range(#?MODULE{indexes = Seq, prev = undefined}) -> ra_seq:range(Seq); range(#?MODULE{indexes = [], prev = Prev}) -> range(Prev); range(#?MODULE{indexes = Seq, prev = Prev}) -> {Start, End} = Range = ra_seq:range(Seq), case ra_range:limit(End, range(Prev)) of undefined -> Range; {PrevStart, _PrevEnd} -> ra_range:new(min(Start, PrevStart), End) end; range(_State) -> undefined. -spec tid(state()) -> ets:tid(). tid(#?MODULE{tid = Tid}) -> Tid. -spec is_active(ets:tid(), state()) -> boolean(). is_active(Tid, State) -> Tid =:= tid(State). -spec prev(state()) -> undefined | state(). prev(#?MODULE{prev = Prev}) -> Prev. -spec indexes(state()) -> ra_seq:state(). indexes(#?MODULE{indexes = Seq}) -> Seq. -spec info(state()) -> map(). info(#?MODULE{tid = Tid, indexes = Seq, prev = Prev} = State) -> #{tid => Tid, size => ets:info(Tid, size), name => ets:info(Tid, name), range => range(State), local_range => ra_seq:range(Seq), previous => case Prev of undefined -> undefined; _ -> info(Prev) end, has_previous => Prev =/= undefined }. -spec record_flushed(ets:tid(), ra_seq:state(), state()) -> {delete_spec(), state()}. record_flushed(TID = Tid, FlushedSeq, #?MODULE{tid = TID, prev = Prev0, indexes = Seq} = State) -> End = ra_seq:last(FlushedSeq), case ra_seq:in(End, Seq) of true -> %% indexes are always written in order so we can delete %% the entire sequence preceeding, this will handle the case %% where a segments notifications is missed Spec0 = {indexes, Tid, ra_seq:limit(End, Seq)}, {Spec, Prev} = case prev_set_first(End + 1, Prev0, true) of {[], P} -> {Spec0, P}; {PSpecs, P} -> {{multi, [Spec0 | PSpecs]}, P} end, NewSeq = ra_seq:floor(End + 1, Seq), {Spec, State#?MODULE{indexes = NewSeq, size = ra_seq:length(NewSeq), prev = Prev}}; false -> {undefined, State} end; record_flushed(_Tid, _FlushedSeq, #?MODULE{prev = undefined} = State) -> {undefined, State}; record_flushed(Tid, FlushedSeq, #?MODULE{prev = Prev0} = State) -> {Spec0, Prev} = record_flushed(Tid, FlushedSeq, Prev0), case range(Prev) of undefined -> Spec = case Spec0 of {multi, Specs} -> %% only emit delete full table specs {multi, [{delete, Tid} | [S || {delete, _} = S <- Specs]]}; {_, Tid, _} -> {delete, Tid}; _ -> Spec0 end, %% the prev table is now empty and can be deleted, {Spec, State#?MODULE{prev = undefined}}; _ -> {Spec0, State#?MODULE{prev = Prev}} end. -spec set_first(ra:index(), state()) -> {[delete_spec()], state()}. set_first(Idx, #?MODULE{tid = Tid, indexes = Seq, prev = Prev0} = State) -> {PrevSpecs, Prev} = prev_set_first(Idx, Prev0, Idx >= ra_seq:first(Seq)), Specs = case Seq of [] -> PrevSpecs; _ -> DeleteSeq = ra_seq:limit(Idx - 1, Seq), [{indexes, Tid, DeleteSeq} | PrevSpecs] end, NewSeq = ra_seq:floor(Idx, Seq), {Specs, State#?MODULE{indexes = NewSeq, size = ra_seq:length(NewSeq), prev = Prev}}. %% Internal prev_set_first(_Idx, undefined, _Force) -> {[], undefined}; prev_set_first(Idx, Prev0, Force) -> case set_first(Idx, Prev0) of {[{indexes, PTID, _} | Rem], #?MODULE{tid = PTID} = P} = Res -> %% set_first/2 returned a range spec for %% prev and prev is now empty, %% upgrade to delete spec of whole tid %% also upgrade if the outer seq is truncated %% by the set_first operation case range_shallow(P) == undefined orelse Force of true -> {[{delete, tid(P)} | Rem], prev(P)}; false -> Res end; Res -> Res end. update_ra_seq(Idx, Seq) -> case ra_seq:last(Seq) of undefined -> ra_seq:append(Idx, Seq); LastIdx when LastIdx == Idx - 1 -> ra_seq:append(Idx, Seq) end. read_sparse(Indexes, State, Acc) -> read_sparse(Indexes, State, 0, Acc). read_sparse([], _State, Num, Acc) -> {Acc, Num, []}; %% no remainder read_sparse([Next | Rem] = Indexes, State, Num, Acc) -> case lookup(Next, State) of undefined -> {Acc, Num, Indexes}; Entry -> read_sparse(Rem, State, Num + 1, [Entry | Acc]) end. range_shallow(#?MODULE{indexes = Seq}) -> ra_seq:range(Seq).