Packages
ra
2.4.7
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-2022 VMware, Inc. or its affiliates. All rights reserved.
%%
-module(ra_snapshot).
-include("ra.hrl").
-type file_err() :: file:posix() | badarg | terminated | system_limit.
%% alias
-type meta() :: snapshot_meta().
-export([
recover/1,
read_meta/2,
begin_read/1,
read_chunk/3,
delete/2,
init/3,
init_ets/0,
current/1,
pending/1,
accepting/1,
directory/1,
last_index_for/1,
begin_snapshot/3,
complete_snapshot/2,
begin_accept/2,
accept_chunk/4,
abort_accept/1,
handle_down/3,
current_snapshot_dir/1
]).
-type effect() :: {monitor, process, snapshot_writer, pid()}.
-export_type([meta/0, file_err/0, effect/0, chunk_flag/0]).
-record(accept, {%% the next expected chunk
next = 1 :: non_neg_integer(),
state :: term(),
idxterm :: ra_idxterm()}).
-record(?MODULE,
{uid :: ra_uid(),
module :: module(),
%% the snapshot directory
%% typically <data_dir>/snapshots
%% snapshot subdirs are store below
%% this as <data_dir>/snapshots/Term_Index
directory :: file:filename(),
pending :: 'maybe'({pid(), ra_idxterm()}),
accepting :: 'maybe'(#accept{}),
current :: 'maybe'(ra_idxterm())}).
-define(ETSTBL, ra_log_snapshot_state).
-opaque state() :: #?MODULE{}.
-export_type([state/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
-callback write(Location :: file:filename(),
Meta :: meta(),
Ref :: term()) ->
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
-callback begin_read(Location :: file:filename()) ->
{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()}.
-spec init(ra_uid(), module(), file:filename()) ->
state().
init(UId, Module, SnapshotsDir) ->
State = #?MODULE{uid = UId,
module = Module,
directory = SnapshotsDir},
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),
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: ~s: skipping ~s as did not validate. Err: ~w",
[UId, S, Err]),
pick_first_valid(UId, Mod, Dir, Rem)
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()) -> 'maybe'(ra_idxterm()).
current(#?MODULE{current = Current}) -> Current.
-spec pending(state()) -> 'maybe'({pid(), ra_idxterm()}).
pending(#?MODULE{pending = Pending}) ->
Pending.
-spec accepting(state()) -> 'maybe'(ra_idxterm()).
accepting(#?MODULE{accepting = undefined}) ->
undefined;
accepting(#?MODULE{accepting = #accept{idxterm = Accepting}}) ->
Accepting.
-spec directory(state()) -> file:filename().
directory(#?MODULE{directory = Dir}) -> Dir.
-spec last_index_for(ra_uid()) -> 'maybe'(ra_index()).
last_index_for(UId) ->
case ets:lookup(?ETSTBL, UId) of
[] -> undefined;
[{_, Index}] -> Index
end.
-spec begin_snapshot(meta(), ReleaseCursorRef :: term(), state()) ->
{state(), [effect()]}.
begin_snapshot(#{index := Idx, term := Term} = Meta, MacRef,
#?MODULE{module = Mod,
directory = Dir} = State) ->
%% 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),
ok = Mod:write(SnapDir, Meta, Ref),
Self ! {ra_log_event,
{snapshot_written, {Idx, Term}}},
ok
end),
%% record snapshot in progress
%% emit an effect that monitors the current snapshot attempt
{State#?MODULE{pending = {Pid, {Idx, Term}}},
[{monitor, process, snapshot_writer, Pid}]}.
-spec complete_snapshot(ra_idxterm(), state()) ->
state().
complete_snapshot({Idx, _} = IdxTerm,
#?MODULE{uid = UId,
module = _Mod,
directory = _Dir} = State) ->
true = ets:insert(?ETSTBL, {UId, Idx}),
State#?MODULE{pending = undefined,
current = IdxTerm}.
-spec begin_accept(meta(), state()) ->
{ok, state()}.
begin_accept(#{index := Idx, term := Term} = Meta,
#?MODULE{module = Mod,
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,
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}},
directory = Dir} = State) ->
ok = delete(Dir, {Idx, Term}),
State#?MODULE{accepting = undefined}.
-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{directory = Dir,
pending = {Pid, IdxTerm}} = State) ->
%% delete the pending snapshot directory
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()) ->
{ok, Meta :: meta(), ReadState} |
{error, term()} when ReadState :: term().
begin_read(#?MODULE{module = Mod,
directory = Dir,
current = {Idx, Term}}) ->
Location = make_snapshot_dir(Dir, Idx, Term),
Mod:begin_read(Location).
-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,
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).
-spec recover(state()) ->
{ok, Meta :: meta(), State :: term()} |
{error, no_current_snapshot} |
{error, term()}.
recover(#?MODULE{current = undefined}) ->
{error, no_current_snapshot};
recover(#?MODULE{module = Mod,
directory = Dir,
current = {Idx, Term}}) ->
SnapDir = make_snapshot_dir(Dir, Idx, Term),
Mod:recover(SnapDir).
-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()) ->
'maybe'(file:filename()).
current_snapshot_dir(#?MODULE{directory = Dir,
current = {Idx, Term}}) ->
make_snapshot_dir(Dir, Idx, Term);
current_snapshot_dir(_) ->
undefined.
%% Utility
make_snapshot_dir(Dir, Index, Term) ->
I = ra_lib:zpad_hex(Index),
T = ra_lib:zpad_hex(Term),
filename:join(Dir, T ++ "_" ++ I).