Packages
ra
2.13.5
3.1.9
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.2
3.0.1
3.0.0
3.0.0-beta.1
2.17.3
2.17.2
2.17.1
2.17.0
2.16.13
2.16.12
2.16.11
2.16.10
2.16.9
2.16.8
2.16.7
2.16.6
2.16.5
2.16.4
2.16.3
2.16.2
2.16.1
2.16.0
2.16.0-pre.12
2.16.0-pre.11
2.16.0-pre.10
2.16.0-pre.9
2.16.0-pre.8
2.16.0-pre.7
2.16.0-pre.6
2.16.0-pre.5
2.16.0-pre.4
2.16.0-pre.3
2.16.0-pre.2
2.16.0-pre.1
2.15.4
2.15.3
2.15.2
2.15.1
2.15.0
2.14.0
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.13.0-pre.1
2.12.0
2.11.0
2.11.0-pre.1
2.10.2-pre.2
2.10.2-pre.1
2.10.1
2.10.0
2.10.0-pre.3
2.10.0-pre.2
2.10.0-pre.1
2.9.10-pre.1
2.9.1
2.9.1-pre.2
2.9.1-pre.1
2.9.0
2.8.0
retired
2.7.3
2.7.2
2.7.1
2.7.0
2.7.0-pre.3
2.7.0-pre.2
2.7.0-pre.1
2.6.3
2.6.2
2.6.1
2.6.0-pre.1
2.5.1
2.5.1-pre.1
2.5.0
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
retired
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.13
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.9.6
0.9.5
0.9.4
0.9.2
0.3.3
retired
0.3.2
retired
0.3.1
retired
Raft library
Current section
Files
Jump to
Current section
Files
src/ra_snapshot.erl
%% 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-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries.
%%
-module(ra_snapshot).
-include("ra.hrl").
-type file_err() :: ra_lib:file_err().
%% alias
-type meta() :: snapshot_meta().
-export([
recover/1,
read_meta/2,
begin_read/2,
read_chunk/3,
delete/2,
init/6,
init_ets/0,
current/1,
pending/1,
accepting/1,
directory/2,
last_index_for/1,
begin_snapshot/4,
promote_checkpoint/2,
complete_snapshot/3,
begin_accept/2,
accept_chunk/4,
abort_accept/1,
context/2,
handle_down/3,
current_snapshot_dir/1,
latest_checkpoint/1,
take_older_checkpoints/2,
take_extra_checkpoints/1
]).
-type effect() :: {monitor, process, snapshot_writer, pid()}.
-type kind() :: snapshot | checkpoint.
-type checkpoint() :: ra_idxterm().
-export_type([
meta/0,
file_err/0,
effect/0,
chunk_flag/0,
kind/0,
checkpoint/0
]).
-record(accept, {%% the next expected chunk
next = 1 :: non_neg_integer(),
state :: term(),
idxterm :: ra_idxterm()}).
-record(?MODULE,
{uid :: ra_uid(),
counter :: undefined | counters:counters_ref(),
module :: module(),
%% typically <data_dir>/snapshots
%% snapshot subdirs are store below
%% this as <data_dir>/snapshots/Term_Index
snapshot_directory :: file:filename(),
%% <data_dir>/checkpoints
%% like snapshots, these are also stored in subdirs
%% as <data_dir>/checkpoints/Term_Index
checkpoint_directory :: file:filename(),
pending :: option({pid(), ra_idxterm(), kind()}),
accepting :: option(#accept{}),
current :: option(ra_idxterm()),
checkpoints = [] :: list(checkpoint()),
max_checkpoints :: pos_integer()}).
-define(ETSTBL, ra_log_snapshot_state).
-opaque state() :: #?MODULE{}.
-export_type([state/0]).
-optional_callbacks([context/0]).
%% Side effect function
%% Turn the current state into immutable reference.
-callback prepare(Index :: ra_index(),
State :: term()) ->
Ref :: term().
%% Saves snapshot from external state to disk.
%% Runs in a separate process.
%% External storage should be available to read
%% `Sync' suggests whether the file should be synchronized with `fsync(1)'.
-callback write(Location :: file:filename(),
Meta :: meta(),
Ref :: term(),
Sync :: boolean()) ->
ok |
{ok, Bytes :: non_neg_integer()} |
{error, file_err() | term()}.
%% Synchronizes the snapshot to disk.
-callback sync(Location :: file:filename()) ->
ok |
{error, file_err() | term()}.
%% Read the snapshot metadata and initialise a read state used in read_chunk/1
%% The read state should contain all the information required to read a chunk
%% The Context is the map returned by the context/0 callback
%% This can be used to inform the sender of receive capabilities.
-callback begin_read(Location :: file:filename(), Context :: map()) ->
{ok, Meta :: meta(), ReadState :: term()}
| {error, term()}.
%% Read a chunk of data from the snapshot using the read state
%% Returns a binary chunk of data and a continuation state
-callback read_chunk(ReadState,
ChunkSizeBytes :: non_neg_integer(),
Location :: file:filename()) ->
{ok, Chunk :: term(), {next, ReadState} | last} | {error, term()}.
%% begin a stateful snapshot acceptance process
-callback begin_accept(SnapDir :: file:filename(),
Meta :: meta()) ->
{ok, AcceptState :: term()} | {error, term()}.
%% accept a chunk of data
-callback accept_chunk(Chunk :: term(),
AcceptState :: term()) ->
{ok, AcceptState :: term()} | {error, term()}.
%% accept the last chunk of data
-callback complete_accept(Chunk :: term(),
AcceptState :: term()) ->
ok | {error, term()}.
%% Side-effect function
%% Recover machine state from file
-callback recover(Location :: file:filename()) ->
{ok, Meta :: meta(), State :: term()} | {error, term()}.
%% validate the integrity of the snapshot
-callback validate(Location :: file:filename()) ->
ok | {error, term()}.
%% Only read meta data from snapshot
-callback read_meta(Location :: file:filename()) ->
{ok, meta()} |
{error, invalid_format |
{invalid_version, integer()} |
checksum_error |
file_err() |
term()}.
-callback context() -> map().
-spec init(ra_uid(), module(), file:filename(), file:filename(),
undefined | counters:counters_ref(), pos_integer()) ->
state().
init(UId, Module, SnapshotsDir, CheckpointDir, Counter, MaxCheckpoints) ->
State = #?MODULE{uid = UId,
counter = Counter,
module = Module,
snapshot_directory = SnapshotsDir,
checkpoint_directory = CheckpointDir,
max_checkpoints = MaxCheckpoints},
State1 = find_snapshots(State),
find_checkpoints(State1).
find_snapshots(#?MODULE{uid = UId,
module = Module,
snapshot_directory = SnapshotsDir} = State) ->
true = ra_lib:is_dir(SnapshotsDir),
{ok, Snaps0} = prim_file:list_dir(SnapshotsDir),
Snaps = lists:reverse(lists:sort(Snaps0)),
%% /snapshots/term_index/
case pick_first_valid(UId, Module, SnapshotsDir, Snaps) of
undefined ->
ok = delete_snapshots(SnapshotsDir, Snaps),
%% initialise snapshots table even if no snapshots have been taken
%% this ensure these is an entry when the WAL queries it
true = ets:insert(?ETSTBL, {UId, -1}),
State;
Current0 ->
Current = filename:join(SnapshotsDir, Current0),
{ok, #{index := Idx, term := Term}} = Module:read_meta(Current),
true = ets:insert(?ETSTBL, {UId, Idx}),
ok = delete_snapshots(SnapshotsDir, lists:delete(Current0, Snaps)),
%% delete old snapshots if any
State#?MODULE{current = {Idx, Term}}
end.
delete_snapshots(Dir, Snaps) ->
Old = [filename:join(Dir, O) || O <- Snaps],
lists:foreach(fun ra_lib:recursive_delete/1, Old),
ok.
pick_first_valid(_, _, _, []) ->
undefined;
pick_first_valid(UId, Mod, Dir, [S | Rem]) ->
case Mod:validate(filename:join(Dir, S)) of
ok -> S;
Err ->
?INFO("ra_snapshot: ~ts: skipping ~s as did not validate. Err: ~w",
[UId, S, Err]),
pick_first_valid(UId, Mod, Dir, Rem)
end.
find_checkpoints(#?MODULE{uid = UId,
module = Module,
current = Current,
checkpoint_directory = CheckpointDir} = State) ->
case ra_lib:is_dir(CheckpointDir) of
false ->
State;
true ->
CurrentIdx = case Current of
undefined ->
-1;
{I, _} ->
I
end,
{ok, CPFiles0} = prim_file:list_dir(CheckpointDir),
CPFiles = lists:reverse(lists:sort(CPFiles0)),
Checkpoints =
lists:filtermap(
fun(File) ->
CP = filename:join(CheckpointDir, File),
case Module:validate(CP) of
ok ->
{ok, #{index := Idx, term := Term}} =
Module:read_meta(CP),
case Idx > CurrentIdx of
true ->
{true, {Idx, Term}};
false ->
?INFO("ra_snapshot: ~ts: removing "
"checkpoint ~s as was older than the "
"current snapshot.",
[UId, CP]),
delete(CheckpointDir, {Idx, Term}),
false
end;
Err ->
?INFO("ra_snapshot: ~ts: removing checkpoint ~s as "
"did not validate. Err: ~w",
[UId, CP, Err]),
ra_lib:recursive_delete(CP),
false
end
end, CPFiles),
State#?MODULE{checkpoints = Checkpoints}
end.
-spec init_ets() -> ok.
init_ets() ->
TableFlags = [set,
named_table,
{read_concurrency, true},
{write_concurrency, true},
public],
_ = ets:new(?ETSTBL, TableFlags),
ok.
-spec current(state()) -> option(ra_idxterm()).
current(#?MODULE{current = Current}) -> Current.
-spec latest_checkpoint(state()) -> option(checkpoint()).
latest_checkpoint(#?MODULE{checkpoints = [Current | _]}) -> Current;
latest_checkpoint(#?MODULE{checkpoints = _}) -> undefined.
-spec pending(state()) -> option({pid(), ra_idxterm(), kind()}).
pending(#?MODULE{pending = Pending}) ->
Pending.
-spec accepting(state()) -> option(ra_idxterm()).
accepting(#?MODULE{accepting = undefined}) ->
undefined;
accepting(#?MODULE{accepting = #accept{idxterm = Accepting}}) ->
Accepting.
-spec directory(state(), kind()) -> file:filename().
directory(#?MODULE{snapshot_directory = Dir}, snapshot) -> Dir;
directory(#?MODULE{checkpoint_directory = Dir}, checkpoint) -> Dir.
-spec last_index_for(ra_uid()) -> option(ra_index()).
last_index_for(UId) ->
case ets:lookup(?ETSTBL, UId) of
[{_, Index}] when Index >= 0 ->
Index;
_ ->
undefined
end.
-spec begin_snapshot(meta(), ReleaseCursorRef :: term(), kind(), state()) ->
{state(), [effect()]}.
begin_snapshot(#{index := Idx, term := Term} = Meta, MacRef, SnapKind,
#?MODULE{module = Mod,
counter = Counter,
snapshot_directory = SnapshotDir,
checkpoint_directory = CheckpointDir} = State) ->
{CounterIdx, Dir} =
case SnapKind of
snapshot ->
{?C_RA_LOG_SNAPSHOT_BYTES_WRITTEN, SnapshotDir};
checkpoint ->
{?C_RA_LOG_CHECKPOINT_BYTES_WRITTEN, CheckpointDir}
end,
%% Snapshots must be fsync'd but checkpoints are OK to not sync.
%% Checkpoints are fsync'd before promotion instead.
Sync = SnapKind =:= snapshot,
%% create directory for this snapshot
SnapDir = make_snapshot_dir(Dir, Idx, Term),
%% call prepare then write_snapshot
%% This needs to be called in the current process to "lock" potentially
%% mutable machine state
Ref = Mod:prepare(Meta, MacRef),
%% write the snapshot in a separate process
Self = self(),
Pid = spawn(fun () ->
ok = ra_lib:make_dir(SnapDir),
case Mod:write(SnapDir, Meta, Ref, Sync) of
ok -> ok;
{ok, BytesWritten} ->
counters_add(Counter, CounterIdx,
BytesWritten),
ok
end,
Self ! {ra_log_event,
{snapshot_written, {Idx, Term}, SnapKind}},
ok
end),
%% record snapshot in progress
%% emit an effect that monitors the current snapshot attempt
{State#?MODULE{pending = {Pid, {Idx, Term}, SnapKind}},
[{monitor, process, snapshot_writer, Pid}]}.
-spec promote_checkpoint(Idx :: ra_index(), State0 :: state()) ->
{boolean(), State :: state(), Effects :: [effect()]}.
promote_checkpoint(PromotionIdx,
#?MODULE{module = Mod,
snapshot_directory = SnapDir,
checkpoint_directory = CheckpointDir,
checkpoints = Checkpoints0} = State0) ->
%% Find the checkpoint with the highest index smaller than or equal to the
%% given `Idx' and rename the checkpoint directory to the snapshot
%% directory.
case find_promotable_checkpoint(PromotionIdx, Checkpoints0, []) of
{Checkpoints, {Idx, Term}} ->
Checkpoint = make_snapshot_dir(CheckpointDir, Idx, Term),
Snapshot = make_snapshot_dir(SnapDir, Idx, Term),
Self = self(),
Pid = spawn(fun() ->
%% Checkpoints are created without calling
%% fsync. Snapshots must be fsync'd though, so
%% sync the checkpoint before promoting it
%% into a snapshot.
ok = Mod:sync(Checkpoint),
ok = prim_file:rename(Checkpoint, Snapshot),
Self ! {ra_log_event,
{snapshot_written,
{Idx, Term}, snapshot}}
end),
State = State0#?MODULE{pending = {Pid, {Idx, Term}, snapshot},
checkpoints = Checkpoints},
{true, State, [{monitor, process, snapshot_writer, Pid}]};
undefined ->
{false, State0, []}
end.
%% Find the first checkpoint smaller than or equal to the promotion index and
%% remove it from the checkpoint list.
-spec find_promotable_checkpoint(PromotionIdx, Checkpoints, Acc) -> Result
when
PromotionIdx :: ra_index(),
Checkpoints :: [ra_idxterm()],
Acc :: [ra_idxterm()],
Result :: option({[ra_idxterm()], ra_idxterm()}).
find_promotable_checkpoint(Idx, [{CPIdx, _} = CP | Rest], Acc)
when CPIdx =< Idx ->
%% Checkpoints are sorted by index descending so the first checkpoint
%% with an index smaller than or equal to the promotion index is the proper
%% checkpoint to promote.
{lists:reverse(Rest, Acc), CP};
find_promotable_checkpoint(Idx, [CP | Rest], Acc) ->
find_promotable_checkpoint(Idx, Rest, [CP | Acc]);
find_promotable_checkpoint(_Idx, [], _Acc) ->
undefined.
-spec complete_snapshot(ra_idxterm(), kind(), state()) ->
state().
complete_snapshot({Idx, _} = IdxTerm, snapshot,
#?MODULE{uid = UId} = State) ->
true = ets:insert(?ETSTBL, {UId, Idx}),
State#?MODULE{pending = undefined,
current = IdxTerm};
complete_snapshot(IdxTerm, checkpoint,
#?MODULE{checkpoints = Checkpoints0} = State) ->
State#?MODULE{pending = undefined,
checkpoints = [IdxTerm | Checkpoints0]}.
-spec begin_accept(meta(), state()) ->
{ok, state()}.
begin_accept(#{index := Idx, term := Term} = Meta,
#?MODULE{module = Mod,
snapshot_directory = Dir} = State) ->
SnapDir = make_snapshot_dir(Dir, Idx, Term),
ok = ra_lib:make_dir(SnapDir),
{ok, AcceptState} = Mod:begin_accept(SnapDir, Meta),
{ok, State#?MODULE{accepting = #accept{idxterm = {Idx, Term},
state = AcceptState}}}.
-spec accept_chunk(term(), non_neg_integer(), chunk_flag(), state()) ->
{ok, state()}.
accept_chunk(Chunk, Num, last,
#?MODULE{uid = UId,
module = Mod,
snapshot_directory = Dir,
current = Current,
accepting = #accept{next = Num,
idxterm = {Idx, _} = IdxTerm,
state = AccState}} = State) ->
%% last chunk
ok = Mod:complete_accept(Chunk, AccState),
%% run validate here?
%% delete the current snapshot if any
_ = spawn(fun () -> delete(Dir, Current) end),
%% update ets table
true = ets:insert(?ETSTBL, {UId, Idx}),
{ok, State#?MODULE{accepting = undefined,
%% reset any pending snapshot writes
pending = undefined,
current = IdxTerm}};
accept_chunk(Chunk, Num, next,
#?MODULE{module = Mod,
accepting =
#accept{state = AccState0,
next = Num} = Accept} = State) ->
{ok, AccState} = Mod:accept_chunk(Chunk, AccState0),
{ok, State#?MODULE{accepting = Accept#accept{state = AccState,
next = Num + 1}}};
accept_chunk(_Chunk, Num, _ChunkFlag,
#?MODULE{accepting = #accept{next = Next}} = State)
when Next > Num ->
%% this must be a resend - we can just ignore it
{ok, State}.
-spec abort_accept(state()) -> state().
abort_accept(#?MODULE{accepting = undefined} = State) ->
State;
abort_accept(#?MODULE{accepting = #accept{idxterm = {Idx, Term}},
snapshot_directory = Dir} = State) ->
ok = delete(Dir, {Idx, Term}),
State#?MODULE{accepting = undefined}.
%% get the snapshot capabilities context of a remote node
-spec context(state(), node()) -> map().
context(#?MODULE{module = Mod}, Node) ->
try erpc:call(Node, Mod, ?FUNCTION_NAME, []) of
Result ->
Result
catch
error:{exception, undef, _} ->
#{}
end.
-spec handle_down(pid(), Info :: term(), state()) ->
state().
handle_down(_Pid, _Info, #?MODULE{pending = undefined} = State) ->
State;
handle_down(_Pid, normal, State) ->
State;
handle_down(_Pid, noproc, State) ->
%% this could happen if the monitor was set up after the process had
%% finished
State;
handle_down(Pid, _Info,
#?MODULE{snapshot_directory = SnapshotDir,
checkpoint_directory = CheckpointDir,
pending = {Pid, IdxTerm, SnapKind}} = State) ->
%% delete the pending snapshot/checkpoint directory
Dir = case SnapKind of
snapshot -> SnapshotDir;
checkpoint -> CheckpointDir
end,
ok = delete(Dir, IdxTerm),
State#?MODULE{pending = undefined}.
delete(_, undefined) ->
ok;
delete(Dir, {Idx, Term}) ->
SnapDir = make_snapshot_dir(Dir, Idx, Term),
ok = ra_lib:recursive_delete(SnapDir),
ok.
-spec begin_read(State :: state(), Context :: map()) ->
{ok, Meta :: meta(), ReadState} |
{error, term()} when ReadState :: term().
begin_read(#?MODULE{module = Mod,
snapshot_directory = Dir,
current = {Idx, Term}},
Context) when is_map(Context) ->
Location = make_snapshot_dir(Dir, Idx, Term),
Mod:begin_read(Location, Context).
-spec read_chunk(ReadState, ChunkSizeBytes :: non_neg_integer(),
State :: state()) ->
{ok, Data :: term(), {next, ReadState} | last} |
{error, term()} when ReadState :: term().
read_chunk(ReadState, ChunkSizeBytes, #?MODULE{module = Mod,
snapshot_directory = Dir,
current = {Idx, Term}}) ->
%% TODO: do we need to generate location for every chunk?
Location = make_snapshot_dir(Dir, Idx, Term),
Mod:read_chunk(ReadState, ChunkSizeBytes, Location).
%% Recovers from the latest checkpoint or snapshot, if available.
-spec recover(state()) ->
{ok, Meta :: meta(), State :: term()} |
{error, no_current_snapshot} |
{error, term()}.
recover(#?MODULE{current = undefined, checkpoints = []}) ->
{error, no_current_snapshot};
recover(#?MODULE{module = Mod,
current = Snapshot,
snapshot_directory = SnapDir,
checkpoints = Checkpoints,
checkpoint_directory = CheckpointDir}) ->
%% If there are checkpoints and a snapshot, recover from whichever has the
%% highest index. Otherwise recover from whichever exists.
Dir = case {Snapshot, Checkpoints} of
{{SnapIdx, _}, [{CPIdx, CPTerm} | _]} when CPIdx > SnapIdx ->
make_snapshot_dir(CheckpointDir, CPIdx, CPTerm);
{{Idx, Term}, _} ->
make_snapshot_dir(SnapDir, Idx, Term);
{undefined, [{Idx, Term} | _]} ->
make_snapshot_dir(CheckpointDir, Idx, Term)
end,
Mod:recover(Dir).
-spec read_meta(Module :: module(), Location :: file:filename()) ->
{ok, meta()} |
{error, invalid_format |
{invalid_version, integer()} |
checksum_error |
file_err() |
term()}.
read_meta(Module, Location) ->
Module:read_meta(Location).
-spec current_snapshot_dir(state()) ->
option(file:filename()).
current_snapshot_dir(#?MODULE{snapshot_directory = Dir,
current = {Idx, Term}}) ->
make_snapshot_dir(Dir, Idx, Term);
current_snapshot_dir(_) ->
undefined.
-spec take_older_checkpoints(ra_index(), state()) ->
{state(), [checkpoint()]}.
take_older_checkpoints(Idx, #?MODULE{checkpoints = Checkpoints0} = State0) ->
{Checkpoints, Outdated} = lists:splitwith(fun ({CPIdx, _Term}) ->
CPIdx > Idx
end, Checkpoints0),
{State0#?MODULE{checkpoints = Checkpoints}, Outdated}.
-spec take_extra_checkpoints(state()) ->
{state(), [checkpoint()]}.
take_extra_checkpoints(State0) ->
take_extra_checkpoints(State0, []).
take_extra_checkpoints(#?MODULE{checkpoints = Checkpoints0,
max_checkpoints = MaxCheckpoints} = State0,
Checks) ->
Len = erlang:length(Checkpoints0),
case Len > MaxCheckpoints of
true ->
%% when the number of checkpoints grow we increase the difference
%% between checkpoints in order to keep the total count kept on disk
%% down but keep some upper limit (~500k) to avoid huge differences
Mult = min(8, Len div MaxCheckpoints),
case find_checkpoint_to_delete(Mult, lists:reverse(Checkpoints0)) of
undefined ->
{State0, Checks};
{_, _} = Check ->
Checkpoints = lists:delete(Check, Checkpoints0),
{State0#?MODULE{checkpoints = Checkpoints},
[Check | Checks]}
end;
false ->
{State0, Checks}
end.
%% Utility
-define(MAX_DIFF, 65_536).
find_checkpoint_to_delete(Mult,
[{FstIdx, _},
{_, _} = Pot,
{ThrdIdx, _} | _] = Checks) ->
case ThrdIdx - FstIdx < (?MAX_DIFF * Mult) of
true ->
Pot;
false ->
find_checkpoint_to_delete(Mult, tl(Checks))
end;
find_checkpoint_to_delete(_, _) ->
undefined.
make_snapshot_dir(Dir, Index, Term) ->
I = ra_lib:zpad_hex(Index),
T = ra_lib:zpad_hex(Term),
filename:join(Dir, T ++ "_" ++ I).
counters_add(undefined, _, _) ->
ok;
counters_add(Counter, Ix, Incr) ->
counters:add(Counter, Ix, Incr).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.