Current section

Files

Jump to
ra src ra_log_segment_writer.erl
Raw

src/ra_log_segment_writer.erl

%% @hidden
-module(ra_log_segment_writer).
-behaviour(gen_server).
-export([start_link/1,
accept_mem_tables/2,
accept_mem_tables/3,
delete_segments/3,
delete_segments/4,
release_segments/2,
my_segments/1,
my_segments/2,
await/0,
await/1,
overview/0
]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-record(state, {data_dir :: file:filename(),
segment_conf = #{} :: ra_log_segment:ra_log_segment_options(),
active_segments = #{} :: #{ra_uid() =>
ra_log_segment:state()}}).
-include("ra.hrl").
-define(AWAIT_TIMEOUT, 30000).
%%% ra_log_segment_writer
%%% receives a set of closed mem_segments from the wal
%%% appends to the current segment for the ra server
%%% notifies the ra server of any new/updates segments
%%%===================================================================
%%% API functions
%%%===================================================================
start_link(Config) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [Config], []).
accept_mem_tables(Tables, WalFile) ->
accept_mem_tables(?MODULE, Tables, WalFile).
accept_mem_tables(_SegmentWriter, [], undefined) ->
ok;
accept_mem_tables(SegmentWriter, Tables, WalFile) ->
gen_server:cast(SegmentWriter, {mem_tables, Tables, WalFile}).
-spec delete_segments(ra_uid(), ra_index(),
[file:filename_all()]) -> ok.
delete_segments(Who, SnapIdx, SegmentFiles) ->
delete_segments(?MODULE, Who, SnapIdx, SegmentFiles).
%% delete segmetns for a ra server. Takes a list of absolute filenames and deletes
%% all but the latest one immedately and sends the latest one to the segment
%% writer process to delete in case it is still open.
-spec delete_segments(atom() | pid(), ra_uid(),
ra_index(), [file:filename_all()]) -> ok.
delete_segments(SegWriter, Who, SnapIdx, SegmentFiles) ->
% delete all closed segment files
gen_server:cast(SegWriter, {delete_segments, Who, SnapIdx, SegmentFiles}).
-spec release_segments(atom() | pid(), ra_uid()) -> ok.
release_segments(SegWriter, Who) ->
gen_server:call(SegWriter, {release_segments, Who}).
%% returns the absolute filenames of all the segments
-spec my_segments(ra_uid()) -> [file:filename()].
my_segments(Who) ->
my_segments(?MODULE, Who).
-spec my_segments(atom() | pid(), ra_uid()) -> [file:filename()].
my_segments(SegWriter, Who) ->
gen_server:call(SegWriter, {my_segments, Who}, infinity).
-spec overview() -> #{}.
overview() ->
gen_server:call(?MODULE, overview).
% used to wait for the segment writer to finish processing anything in flight
await() ->
await(?MODULE).
await(SegWriter) ->
IsAlive = fun IsAlive(undefined) -> false;
IsAlive(P) when is_pid(P) ->
is_process_alive(P);
IsAlive(A) when is_atom(A) ->
IsAlive(whereis(A))
end,
case IsAlive(SegWriter) of
true ->
gen_server:call(SegWriter, await, ?AWAIT_TIMEOUT);
false ->
% if it is down it isn't processing anything
ok
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([#{data_dir := DataDir} = Conf]) ->
process_flag(trap_exit, true),
SegmentConf = maps:get(segment_conf, Conf, #{}),
{ok, #state{data_dir = DataDir,
segment_conf = SegmentConf}}.
handle_call(await, _From, State) ->
{reply, ok, State};
handle_call({release_segments, Who}, _From,
#state{active_segments = ActiveSegments0} = State0) ->
case maps:take(Who, ActiveSegments0) of
{Seg, ActiveSegments} ->
% defensive
_ = ra_log_segment:close(Seg),
{reply, ok, State0#state{active_segments = ActiveSegments}};
error ->
{reply, ok, State0}
end;
handle_call({my_segments, Who}, _From,
#state{data_dir = DataDir} = State) ->
Dir = filename:join(DataDir, ra_lib:to_list(Who)),
SegFiles = lists:sort(filelib:wildcard(filename:join(Dir, "*.segment"))),
{reply, SegFiles, State};
handle_call(overview, _From, State) ->
{reply, get_overview(State), State}.
handle_cast({mem_tables, Tables, WalFile}, State0) ->
State = lists:foldl(fun do_segment/2, State0, Tables),
% delete wal file once done
% TODO: test scenario when server crashes after segments but before
% deleting walfile
% can we make segment writer idempotent somehow
?INFO("segment_writer: deleting wal file: ~s~n",
[filename:basename(WalFile)]),
%% temporarily disable wal deletion
%% TODO: this shoudl be a debug option config?
% Base = filename:basename(WalFile),
% BkFile = filename:join([State0#state.data_dir, "wals", Base]),
% filelib:ensure_dir(BkFile),
% file:copy(WalFile, BkFile),
_ = file:delete(WalFile),
%% ensure we release any bin refs that might have been acquired during
%% segment write
true = erlang:garbage_collect(),
{noreply, State};
handle_cast({delete_segments, Who, Idx, [SegmentFile | SegmentFiles]},
#state{active_segments = ActiveSegments} = State0) ->
[_ = file:delete(F) || F <- SegmentFiles],
case ActiveSegments of
#{Who := Seg} ->
case ra_log_segment:is_same_as(Seg, SegmentFile) of
true ->
% the segment file is the correct one
case ra_log_segment:range(Seg) of
{_From, To} when To =< Idx ->
% segment can be deleted
ok = ra_log_segment:close(Seg),
ok = file:delete(SegmentFile),
{noreply,
State0#state{active_segments =
maps:remove(Who, ActiveSegments)}};
_ ->
{noreply, State0}
end;
false ->
%% don't validate the file got deleted as it is possible
%% to get multiple requests under high load
_ = file:delete(SegmentFile),
{noreply, State0}
end;
_ ->
% if it isn't active we can just delete it
_ = file:delete(SegmentFile),
{noreply, State0}
end.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{active_segments = ActiveSegments}) ->
% ensure any open segments are closed
[ok = ra_log_segment:close(Seg)
|| Seg <- maps:values(ActiveSegments)],
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
get_overview(#state{active_segments = Active,
segment_conf = Conf}) ->
#{active_segments => maps:size(Active),
segment_conf => Conf
}.
do_segment({ServerUId, StartIdx0, EndIdx, Tid},
#state{data_dir = DataDir,
segment_conf = SegConf,
active_segments = ActiveSegments} = State) ->
Dir = filename:join(DataDir, binary_to_list(ServerUId)),
case filelib:is_dir(Dir) of
true ->
%% check that the directory exists -
%% if it does not exist the server has
%% never been started or has been deleted
%% and we should just continue
Segment0 = case ActiveSegments of
#{ServerUId := S} -> S;
_ -> open_file(Dir, SegConf)
end,
case Segment0 of
undefined ->
State;
_ ->
{Segment1, Closed0} =
append_to_segment(ServerUId, Tid, StartIdx0, EndIdx,
Segment0, SegConf),
% fsync
{ok, Segment} = ra_log_segment:sync(Segment1),
% notify writerid of new segment update
% includes the full range of the segment
ClosedSegRefs = [ra_log_segment:segref(S) || S <- Closed0],
SegRefs = case ra_log_segment:segref(Segment) of
undefined ->
ClosedSegRefs;
SRef ->
[SRef | ClosedSegRefs]
end,
ok = send_segments(ServerUId, Tid, SegRefs),
State#state{active_segments =
ActiveSegments#{ServerUId => Segment}}
end;
false ->
?INFO("segment_writer: skipping segment as directory ~s does "
"not exist~n", [Dir]),
State
end.
start_index(ServerUId, StartIdx0) ->
case ets:lookup(ra_log_snapshot_state, ServerUId) of
[{_, SnapIdx}] ->
max(SnapIdx + 1, StartIdx0);
[] ->
StartIdx0
end.
send_segments(ServerUId, Tid, Segments) ->
Msg = {ra_log_event, {segments, Tid, Segments}},
try ra_directory:send(ServerUId, Msg) of
_ -> ok
catch
ErrType:Err ->
?INFO("ra_log_segment_writer: error sending "
"ra_log_event to: "
"~s. Error:~n~w:~W~n",
[ServerUId, ErrType, Err, 7]),
ok
end.
append_to_segment(UId, Tid, StartIdx0, EndIdx, Seg, SegConf) ->
StartIdx = start_index(UId, StartIdx0),
% EndIdx + 1 because FP
append_to_segment(UId, Tid, StartIdx, EndIdx+1, Seg, [], SegConf).
append_to_segment(_, _, StartIdx, EndIdx, Seg, Closed, _)
when StartIdx >= EndIdx ->
{Seg, Closed};
append_to_segment(UId, Tid, Idx, EndIdx, Seg0, Closed, SegConf) ->
[{_, Term, Data0}] = ets:lookup(Tid, Idx),
Data = term_to_binary(Data0),
case ra_log_segment:append(Seg0, Idx, Term, Data) of
{ok, Seg} ->
append_to_segment(UId, Tid, Idx+1, EndIdx, Seg, Closed, SegConf);
{error, full} ->
% close and open a new segment
Seg = open_successor_segment(Seg0, SegConf),
%% re-evaluate snapshot state for the server in case a snapshot
%% has completed during segmeng flush
StartIdx = start_index(UId, Idx),
% recurse
% TODO: there is a micro-inefficiency here in that we need to
% call term_to_binary and do an ETS lookup again that we have
% already done.
append_to_segment(UId, Tid, StartIdx, EndIdx, Seg,
[Seg0 | Closed], SegConf)
end.
find_segment_files(Dir) ->
lists:reverse(
lists:sort(filelib:wildcard(filename:join(Dir, "*.segment")))).
open_successor_segment(CurSeg, SegConf) ->
Fn0 = ra_log_segment:filename(CurSeg),
Fn = ra_lib:zpad_filename_incr(Fn0),
ok = ra_log_segment:close(CurSeg),
{ok, Seg} = ra_log_segment:open(Fn, SegConf),
Seg.
open_file(Dir, SegConf) ->
File = case find_segment_files(Dir) of
[] ->
F = ra_lib:zpad_filename("", "segment", 1),
Fn = filename:join(Dir, F),
_ = file:make_dir(Dir),
Fn;
[F | _] ->
F
end,
%% there is a small chance we'll get here without the target directory
%% existing this could happend during server deletion
case filelib:ensure_dir(File) of
ok ->
case ra_log_segment:open(File, SegConf#{mode => append}) of
{ok, Segment} ->
Segment;
Err ->
?WARN("segment_writer: failed to open segment file ~w"
"error: ~W", [File, Err]),
undefined
end;
{error, Err} ->
?WARN("segment_writer: failed to create directory ~w, Err: ~w~n",
[File, Err]),
undefined
end.