%% 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-2022 VMware, Inc. or its affiliates. All rights reserved. %% -module(ra_log_reader). -compile(inline_list_funcs). -export([ init/6, init/8, close/1, update_segments/2, handle_log_update/2, segment_refs/1, num_open_segments/1, update_first_index/2, read/3, read/4, sparse_read/3, fetch_term/2, delete_closed_mem_table_object/2, closed_mem_tables/1, open_mem_table_lookup/1 ]). -include("ra.hrl"). -define(STATE, ?MODULE). -type access_pattern() :: sequential | random. %% holds static or rarely changing fields -record(cfg, {uid :: ra_uid(), counter :: undefined | counters:counters_ref(), directory :: file:filename(), open_mem_tbls :: atom(), closed_mem_tbls :: atom(), access_pattern = random :: access_pattern() }). -type segment_ref() :: {From :: ra_index(), To :: ra_index(), File :: string()}. -record(?STATE, {cfg :: #cfg{}, first_index = 0 :: ra_index(), segment_refs = [] :: [segment_ref()], open_segments = ra_flru:new(1, fun flru_handler/1) :: ra_flru:state() }). -opaque state() :: #?STATE{}. -export_type([ state/0 ]). %% PUBLIC -spec init(ra_uid(), file:filename(), ra_index(), non_neg_integer(), [segment_ref()], ra_system:names()) -> state(). init(UId, Dir, FirstIdx, MaxOpen, SegRefs, Names) -> init(UId, Dir, FirstIdx, MaxOpen, random, SegRefs, Names, undefined). -spec init(ra_uid(), file:filename(), ra_index(), non_neg_integer(), access_pattern(), [segment_ref()], ra_system:names(), undefined | counters:counters_ref()) -> state(). init(UId, Dir, FirstIdx, MaxOpen, AccessPattern, SegRefs, #{open_mem_tbls := OpnMemTbls, closed_mem_tbls := ClsdMemTbls}, Counter) when is_binary(UId) -> #?STATE{cfg = #cfg{uid = UId, counter = Counter, directory = Dir, open_mem_tbls = OpnMemTbls, closed_mem_tbls = ClsdMemTbls, access_pattern = AccessPattern }, open_segments = ra_flru:new(MaxOpen, fun flru_handler/1), first_index = FirstIdx, segment_refs = SegRefs}. -spec close(state()) -> ok. close(#?STATE{open_segments = Open}) -> _ = ra_flru:evict_all(Open), ok. -spec update_segments([segment_ref()], state()) -> state(). update_segments(NewSegmentRefs, #?STATE{open_segments = Open0, segment_refs = SegmentRefs0} = State) -> SegmentRefs = compact_seg_refs(NewSegmentRefs ++ SegmentRefs0), %% check if any of the updated segrefs refer to open segments %% we close these segments so that they can be re-opened with updated %% indexes if needed Open = lists:foldl(fun ({_, _, F}, Acc0) -> case ra_flru:evict(F, Acc0) of {_, Acc} -> Acc; error -> Acc0 end end, Open0, SegmentRefs), State#?MODULE{segment_refs = SegmentRefs, open_segments = Open}. -spec handle_log_update({ra_log_update, undefined | pid(), ra_index(), [segment_ref()]}, state()) -> state(). handle_log_update({ra_log_update, From, FstIdx, SegRefs}, #?STATE{open_segments = Open0} = State) -> Open = ra_flru:evict_all(Open0), case From of undefined -> ok; _ -> %% reply to the updater process From ! ra_log_update_processed end, State#?MODULE{segment_refs = SegRefs, first_index = FstIdx, open_segments = Open}. -spec update_first_index(ra_index(), state()) -> {state(), [segment_ref()]}. update_first_index(Idx, #?STATE{segment_refs = SegRefs0, open_segments = OpenSegs0} = State) -> case lists:partition(fun({_, To, _}) when To >= Idx -> true; (_) -> false end, SegRefs0) of {_, []} -> {State, []}; {Active, Obsolete} -> ObsoleteKeys = [element(3, O) || O <- Obsolete], % close any open segments OpenSegs = lists:foldl(fun (K, OS0) -> case ra_flru:evict(K, OS0) of {_, OS} -> OS; error -> OS0 end end, OpenSegs0, ObsoleteKeys), {State#?STATE{open_segments = OpenSegs, first_index = Idx + 1, segment_refs = Active}, Obsolete} end. -spec segment_refs(state()) -> [segment_ref()]. segment_refs(#?STATE{segment_refs = SegmentRefs}) -> SegmentRefs. -spec num_open_segments(state()) -> non_neg_integer(). num_open_segments(#?STATE{open_segments = Open}) -> ra_flru:size(Open). -spec read(ra_index(), ra_index(), state()) -> {[log_entry()], NumRead :: non_neg_integer(), state()}. read(From, To, State) -> read(From, To, State, []). -spec read(ra_index(), ra_index(), state(), [log_entry()]) -> {[log_entry()], NumRead :: non_neg_integer(), state()}. read(From, To, State, Entries) when From =< To -> retry_read(2, From, To, Entries, State); read(_From, _To, State, Entries) -> {Entries, 0, State}. -spec sparse_read(state(), [ra_index()], [log_entry()]) -> {[log_entry()], state()}. sparse_read(#?STATE{cfg = #cfg{} = Cfg} = State, Indexes0, Entries0) -> case open_mem_tbl_sparse_read(Cfg, Indexes0, Entries0) of {Entries1, OpenC, []} -> ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPEN_MEM_TBL, OpenC), {Entries1, State}; {Entries1, OpenC, Rem1} -> ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPEN_MEM_TBL, OpenC), case closed_mem_tbl_sparse_read(Cfg, Rem1, Entries1) of {Entries2, ClosedC, []} -> ok = incr_counter(Cfg, ?C_RA_LOG_READ_CLOSED_MEM_TBL, ClosedC), {Entries2, State}; {Entries2, ClosedC, Rem2} -> ok = incr_counter(Cfg, ?C_RA_LOG_READ_CLOSED_MEM_TBL, ClosedC), {Open, _, SegC, Entries} = (catch segment_sparse_read(State, Rem2, Entries2)), ok = incr_counter(Cfg, ?C_RA_LOG_READ_SEGMENT, SegC), {Entries, State#?MODULE{open_segments = Open}} end end. retry_read(0, From, To, _Entries0, State) -> exit({ra_log_reader_reader_retry_exhausted, From, To, State}); retry_read(N, From, To, Entries0, #?STATE{cfg = #cfg{uid = UId, open_mem_tbls = OpenTbl, closed_mem_tbls = ClosedTbl} = Cfg} = State) -> % 2. Check open mem table % 3. Check closed mem tables in turn % 4. Check on disk segments in turn case open_mem_tbl_take(OpenTbl, UId, {From, To}, Entries0) of {Entries1, {_, C} = Counter0, undefined} -> ok = incr_counter(Cfg, Counter0), {Entries1, C, State}; {Entries1, {_, C0} = Counter0, Rem1} -> ok = incr_counter(Cfg, Counter0), case catch closed_mem_tbl_take(ClosedTbl, UId, Rem1, Entries1) of {Entries2, {_, C1} = Counter1, undefined} -> ok = incr_counter(Cfg, Counter1), {Entries2, C0 + C1, State}; {Entries2, {_, C1} = Counter1, {S, E} = Rem2} -> ok = incr_counter(Cfg, Counter1), case catch segment_take(State, Rem2, Entries2) of {Open, undefined, Entries} -> C = (E - S + 1) + C0 + C1, incr_counter(Cfg, {?C_RA_LOG_READ_SEGMENT, E - S + 1}), {Entries, C, State#?MODULE{open_segments = Open}} end; {ets_miss, _Index} -> %% this would happen if a mem table was deleted after %% an external reader had read the range retry_read(N-1, From, To, Entries0, State) end end. -spec fetch_term(ra_index(), state()) -> {ra_index(), state()}. fetch_term(Idx, #?STATE{cfg = #cfg{uid = UId, open_mem_tbls = OpenTbl, closed_mem_tbls = ClosedTbl} = Cfg} = State0) -> incr_counter(Cfg, {?C_RA_LOG_FETCH_TERM, 1}), case ets:lookup(OpenTbl, UId) of [{_, From, To, Tid}] when Idx >= From andalso Idx =< To -> Term = ets:lookup_element(Tid, Idx, 2), {Term, State0}; _ -> case closed_mem_table_term_query(ClosedTbl, Idx, UId) of undefined -> segment_term_query(Idx, State0); Term -> {Term, State0} end end. -spec delete_closed_mem_table_object(state(), term()) -> true. delete_closed_mem_table_object(#?STATE{cfg = #cfg{closed_mem_tbls = Tbl}}, Id) -> true = ets:delete_object(Tbl, Id). -spec closed_mem_tables(state()) -> list(). closed_mem_tables(#?STATE{cfg = #cfg{uid = UId, closed_mem_tbls = Tbl}}) -> closed_mem_tables(Tbl, UId). -spec open_mem_table_lookup(state()) -> list(). open_mem_table_lookup(#?STATE{cfg = #cfg{uid = UId, open_mem_tbls = Tbl}}) -> ets:lookup(Tbl, UId). %% LOCAL segment_term_query(Idx, #?MODULE{segment_refs = SegRefs, cfg = Cfg, open_segments = OpenSegs} = State) -> {Result, Open} = segment_term_query0(Idx, SegRefs, OpenSegs, Cfg), {Result, State#?MODULE{open_segments = Open}}. segment_term_query0(Idx, [{From, To, Filename} | _], Open0, #cfg{directory = Dir, access_pattern = AccessPattern}) when Idx >= From andalso Idx =< To -> case ra_flru:fetch(Filename, Open0) of {ok, Seg, Open} -> Term = ra_log_segment:term_query(Seg, Idx), {Term, Open}; error -> AbsFn = filename:join(Dir, Filename), {ok, Seg} = ra_log_segment:open(AbsFn, #{mode => read, access_pattern => AccessPattern}), Term = ra_log_segment:term_query(Seg, Idx), {Term, ra_flru:insert(Filename, Seg, Open0)} end; segment_term_query0(Idx, [_ | Tail], Open, Cfg) -> segment_term_query0(Idx, Tail, Open, Cfg); segment_term_query0(_Idx, [], Open, _) -> {undefined, Open}. open_mem_tbl_sparse_read(#cfg{uid = UId, open_mem_tbls = OpenTbl}, Indexes, Acc0) -> case ets:lookup(OpenTbl, UId) of [{_, TStart, TEnd, Tid}] -> mem_tbl_sparse_read(Indexes, TStart, TEnd, Tid, 0, Acc0); [] -> {Acc0, 0, Indexes} end. closed_mem_tbl_sparse_read(#cfg{uid = UId, closed_mem_tbls = ClosedTbl}, Indexes, Acc0) -> case closed_mem_tables(ClosedTbl, UId) of [] -> {Acc0, 0, Indexes}; Tables -> lists:foldl(fun({_, _, TblSt, TblEnd, Tid}, {Ac, Num, Idxs}) -> mem_tbl_sparse_read(Idxs, TblSt, TblEnd, Tid, Num, Ac) end, {Acc0, 0, Indexes}, Tables) end. mem_tbl_sparse_read([I | Rem], TblStart, TblEnd, Tid, C, Entries0) when I >= TblStart andalso I =< TblEnd -> [Entry] = ets:lookup(Tid, I), mem_tbl_sparse_read(Rem, TblStart, TblEnd, Tid, C + 1, [Entry | Entries0]); mem_tbl_sparse_read(Rem, _TblStart, _TblEnd, _Tid, C, Entries0) -> {Entries0, C, Rem}. open_mem_tbl_take(OpenTbl, Id, {Start0, End}, Acc0) -> case ets:lookup(OpenTbl, Id) of [{_, TStart, TEnd, Tid}] -> {Entries, Count, Rem} = mem_tbl_take({Start0, End}, TStart, TEnd, Tid, 0, Acc0), {Entries, {?C_RA_LOG_READ_OPEN_MEM_TBL, Count}, Rem}; [] -> {Acc0, {?C_RA_LOG_READ_OPEN_MEM_TBL, 0}, {Start0, End}} end. closed_mem_tbl_take(ClosedTbl, Id, {Start0, End}, Acc0) -> case closed_mem_tables(ClosedTbl, Id) of [] -> {Acc0, {?C_RA_LOG_READ_CLOSED_MEM_TBL, 0}, {Start0, End}}; Tables -> {Entries, Count, Rem} = lists:foldl(fun({_, _, TblSt, TblEnd, Tid}, {Ac, Count, Range}) -> mem_tbl_take(Range, TblSt, TblEnd, Tid, Count, Ac) end, {Acc0, 0, {Start0, End}}, Tables), {Entries, {?C_RA_LOG_READ_CLOSED_MEM_TBL, Count}, Rem} end. mem_tbl_take(undefined, _TblStart, _TblEnd, _Tid, Count, Acc0) -> {Acc0, Count, undefined}; mem_tbl_take({_Start0, End} = Range, TblStart, _TblEnd, _Tid, Count, Acc0) when TblStart > End -> % optimisation to bypass request that has no overlap {Acc0, Count, Range}; mem_tbl_take({Start0, End}, TblStart, TblEnd, Tid, Count, Acc0) when TblEnd >= End -> Start = max(TblStart, Start0), Entries = lookup_range(Tid, Start, End, Acc0), Remainder = case Start =:= Start0 of true -> % the range was fully covered by the mem table undefined; false -> {Start0, Start-1} end, {Entries, Count + (End - Start + 1), Remainder}; mem_tbl_take({Start0, End}, TblStart, TblEnd, Tid, Count, Acc0) when TblEnd < End -> %% defensive case - truncate the read to end at table end mem_tbl_take({Start0, TblEnd}, TblStart, TblEnd, Tid, Count, Acc0). lookup_range(Tid, Start, Start, Acc) -> try ets:lookup(Tid, Start) of [Entry] -> [Entry | Acc] catch error:badarg -> throw({ets_miss, Start}) end; lookup_range(Tid, Start, End, Acc) when End > Start -> try ets:lookup(Tid, End) of [Entry] -> lookup_range(Tid, Start, End-1, [Entry | Acc]) catch error:badarg -> throw({ets_miss, Start}) end. segment_take(#?STATE{segment_refs = [], open_segments = Open}, _Range, Entries0) -> {Open, undefined, Entries0}; segment_take(#?STATE{segment_refs = [{_From, SEnd, _Fn} | _] = SegRefs, open_segments = OpenSegs, cfg = Cfg}, {RStart, REnd}, Entries0) -> Range = {RStart, min(SEnd, REnd)}, lists:foldl( fun(_, {_, undefined, _} = Acc) -> %% we're done reading throw(Acc); ({From, _, _}, {_, {_, End}, _} = Acc) when From > End -> Acc; ({From, To, Fn}, {Open0, {Start0, End}, E0}) when To >= End -> {Seg, Open} = get_segment(Cfg, Open0, Fn), % actual start point cannot be prior to first segment % index Start = max(Start0, From), Num = End - Start + 1, Entries = ra_log_segment:read_cons(Seg, Start, Num, fun binary_to_term/1, E0), Rem = case Start of Start0 -> undefined; _ -> {Start0, Start-1} end, {Open, Rem, Entries} end, {OpenSegs, Range, Entries0}, SegRefs). segment_sparse_read(#?STATE{open_segments = Open}, [], Entries0) -> {Open, [], 0, Entries0}; segment_sparse_read(#?STATE{segment_refs = SegRefs, open_segments = OpenSegs, cfg = Cfg}, Indexes, Entries0) -> lists:foldl( fun(_, {_, [], _, _} = Acc) -> %% we're done reading throw(Acc); ({From, To, Fn}, {Open0, [NextIdx | _] = Idxs, C, En0}) when NextIdx >= From andalso NextIdx =< To -> {Seg, Open} = get_segment(Cfg, Open0, Fn), {ReadIdxs, RemIdxs} = sparse_read_split(fun (I) -> I >= From andalso I =< To end, Idxs, []), {_Cache, ReadSparseCount, Entries} = ra_log_segment:read_sparse(Seg, ReadIdxs, fun binary_to_term/1, []), {Open, RemIdxs, C + ReadSparseCount, lists:reverse(Entries, En0)}; (_Segref, Acc) -> Acc end, {OpenSegs, Indexes, 0, Entries0}, SegRefs). flru_handler({_, Seg}) -> _ = ra_log_segment:close(Seg), ok. %% like lists:splitwith but without reversing the accumulator sparse_read_split(Fun, [E | Rem] = All, Acc) -> case Fun(E) of true -> sparse_read_split(Fun, Rem, [E | Acc]); false -> {Acc, All} end; sparse_read_split(_Fun, [], Acc) -> {Acc, []}. get_segment(#cfg{directory = Dir, access_pattern = AccessPattern}, Open0, Fn) -> case ra_flru:fetch(Fn, Open0) of {ok, S, Open1} -> {S, Open1}; error -> AbsFn = filename:join(Dir, Fn), case ra_log_segment:open(AbsFn, #{mode => read, access_pattern => AccessPattern}) of {ok, S} -> {S, ra_flru:insert(Fn, S, Open0)}; {error, Err} -> exit({ra_log_failed_to_open_segment, Err, AbsFn}) end end. closed_mem_tables(Tbl, Id) -> case ets:lookup(Tbl, Id) of [] -> []; Tables -> lists:sort(fun (A, B) -> element(2, A) > element(2, B) end, Tables) end. closed_mem_table_term_query(Tbl, Idx, Id) -> case closed_mem_tables(Tbl, Id) of [] -> undefined; Tables -> closed_mem_table_term_query0(Idx, Tables) end. closed_mem_table_term_query0(_Idx, []) -> undefined; closed_mem_table_term_query0(Idx, [{_, _, From, To, Tid} | _Tail]) when Idx >= From andalso Idx =< To -> ets:lookup_element(Tid, Idx, 2); closed_mem_table_term_query0(Idx, [_ | Tail]) -> closed_mem_table_term_query0(Idx, Tail). compact_seg_refs(SegRefs) -> lists:reverse( lists:foldl( fun ({_, _, File} = S, Acc) -> case lists:any(fun({_, _, F}) when F =:= File -> true; (_) -> false end, Acc) of true -> Acc; false -> [S | Acc] end end, [], SegRefs)). incr_counter(#cfg{counter = Cnt}, Ix, N) when Cnt =/= undefined -> counters:add(Cnt, Ix, N); incr_counter(#cfg{counter = undefined}, _, _) -> ok. incr_counter(#cfg{counter = Cnt}, {Ix, N}) when Cnt =/= undefined -> counters:add(Cnt, Ix, N); incr_counter(#cfg{counter = undefined}, _) -> ok. -ifdef(TEST). open_mem_tbl_take_test() -> OTbl = ra_log_open_mem_tables, _ = ets:new(OTbl, [named_table]), Tid = ets:new(test_id, []), true = ets:insert(OTbl, {test_id, 3, 7, Tid}), Entries = [{3, 2, "3"}, {4, 2, "4"}, {5, 2, "5"}, {6, 2, "6"}, {7, 2, "7"}], % seed the mem table [ets:insert(Tid, E) || E <- Entries], {Entries, _, undefined} = open_mem_tbl_take(OTbl, test_id, {3, 7}, []), EntriesPlus8 = Entries ++ [{8, 2, "8"}], {EntriesPlus8, _, {1, 2}} = open_mem_tbl_take(OTbl, test_id, {1, 7}, [{8, 2, "8"}]), {[{6, 2, "6"}], _, undefined} = open_mem_tbl_take(OTbl, test_id, {6, 6}, []), {[], _, {1, 2}} = open_mem_tbl_take(OTbl, test_id, {1, 2}, []), ets:delete(Tid), ets:delete(OTbl), ok. closed_mem_tbl_take_test() -> CTbl = ra_log_closed_mem_tables, _ = ets:new(CTbl, [named_table, bag]), Tid1 = ets:new(test_id, []), Tid2 = ets:new(test_id, []), M1 = erlang:unique_integer([monotonic, positive]), M2 = erlang:unique_integer([monotonic, positive]), true = ets:insert(CTbl, {test_id, M1, 5, 7, Tid1}), true = ets:insert(CTbl, {test_id, M2, 8, 10, Tid2}), Entries1 = [{5, 2, "5"}, {6, 2, "6"}, {7, 2, "7"}], Entries2 = [{8, 2, "8"}, {9, 2, "9"}, {10, 2, "10"}], % seed the mem tables [ets:insert(Tid1, E) || E <- Entries1], [ets:insert(Tid2, E) || E <- Entries2], {Entries1, _, undefined} = closed_mem_tbl_take(CTbl, test_id, {5, 7}, []), {Entries2, _, undefined} = closed_mem_tbl_take(CTbl, test_id, {8, 10}, []), {[{9, 2, "9"}], _, undefined} = closed_mem_tbl_take(CTbl, test_id, {9, 9}, []), ok. compact_seg_refs_test() -> % {From, To, File} Refs = [{10, 100, "2"}, {10, 75, "2"}, {10, 50, "2"}, {1, 9, "1"}], [{10, 100, "2"}, {1, 9, "1"}] = compact_seg_refs(Refs), ok. -endif.