Add hybrid mnesia_rocksdb backup and restore
Provide mnesia_rocksdb:backup/1,2 and restore/1,2 that pair a Mnesia checkpoint (schema and non-RocksDB tables) with RocksDB backup engines for open admin and standalone databases. Restore stages RocksDB into a sibling directory and redirects mountpoints while Mnesia fallback purges the real directory, then relocates the data and optionally restarts. Raise the minimum OTP version to 27.
This commit is contained in:
@@ -43,7 +43,7 @@ is provided.
|
|||||||
|
|
||||||
* sext (included as dependency)
|
* sext (included as dependency)
|
||||||
|
|
||||||
* Erlang/OTP 21.0 or newer (https://github.com/erlang/otp)
|
* Erlang/OTP 27.0 or newer (https://github.com/erlang/otp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -115,6 +115,65 @@ source code in `mnesia_rocksdb_tuning.erl` and `mnesia_rocksdb_params.erl`.
|
|||||||
Contributions are welcome.
|
Contributions are welcome.
|
||||||
|
|
||||||
|
|
||||||
|
#### <a name="Backup">Backup</a> ####
|
||||||
|
|
||||||
|
`mnesia_rocksdb:backup(Dir)` creates a **local hybrid backup** of Mnesia
|
||||||
|
metadata/tables and RocksDB data. Unlike `mnesia:backup/1`, RocksDB-backed
|
||||||
|
tables are not dumped record-by-record through Mnesia; they are captured with
|
||||||
|
the [RocksDB backup engine](https://rocksdb.hexdocs.pm/1.9.0/how_to_backup_rocksdb.html).
|
||||||
|
|
||||||
|
Layout of `Dir` after a successful backup:
|
||||||
|
|
||||||
|
```
|
||||||
|
Dir/
|
||||||
|
MANIFEST % restore metadata (Erlang term)
|
||||||
|
mnesia.BUP % schema + non-RocksDB tables
|
||||||
|
rocksdb/<alias>/admin/ % backup engine dir for each alias admin DB
|
||||||
|
rocksdb/standalone/... % one dir per open standalone RocksDB table
|
||||||
|
```
|
||||||
|
|
||||||
|
Algorithm (local node only):
|
||||||
|
|
||||||
|
1. Activate a Mnesia checkpoint on **schema and non-RocksDB tables only**
|
||||||
|
(so large RocksDB tables do not get expensive checkpoint retainers).
|
||||||
|
2. For each open RocksDB instance (admin DB per alias, and standalone tables),
|
||||||
|
run `rocksdb:create_new_backup/2`.
|
||||||
|
3. Dump the checkpoint with `mnesia:backup_checkpoint/3`, excluding RocksDB
|
||||||
|
table data. Schema still describes RocksDB tables.
|
||||||
|
4. Write `MANIFEST`.
|
||||||
|
|
||||||
|
Options map (second argument to `backup/2`):
|
||||||
|
|
||||||
|
* `{verify, true}` — call `rocksdb:verify_backup/2` after each RocksDB backup
|
||||||
|
* `{mnesia_backup_module, Mod}` — override the Mnesia backup media module
|
||||||
|
|
||||||
|
#### Restore
|
||||||
|
|
||||||
|
`mnesia_rocksdb:restore(Dir)` restores a hybrid backup offline (local node).
|
||||||
|
|
||||||
|
Mnesia applies fallbacks by **purging** the Mnesia directory. RocksDB data is
|
||||||
|
therefore restored first into a **sibling staging directory** and mountpoints
|
||||||
|
are redirected until the fallback has finished:
|
||||||
|
|
||||||
|
1. Stop Mnesia (closes RocksDB handles).
|
||||||
|
2. Restore each RocksDB DB into
|
||||||
|
`"<basename(mnesia_dir)>.rocksdb_restore/"` (sibling of the Mnesia dir).
|
||||||
|
3. Set application env `mnesia_rocksdb.restore_mountpoint_root` so
|
||||||
|
`data_mountpoint/1` opens DBs from staging.
|
||||||
|
4. Install `Dir/mnesia.BUP` and start Mnesia (fallback purges the real Mnesia
|
||||||
|
dir; RocksDB tables load from staging with full data). During this window,
|
||||||
|
`delete_table` does not destroy staged RocksDB files.
|
||||||
|
5. Stop Mnesia, clear the redirect, move staged `*.extrdb` directories into the
|
||||||
|
real Mnesia directory.
|
||||||
|
6. Optionally start Mnesia (`#{start => true}`).
|
||||||
|
|
||||||
|
Common options for `restore/2`:
|
||||||
|
|
||||||
|
* `{stop, true|false}` — stop Mnesia before restore (default `true`)
|
||||||
|
* `{start, true|false}` — start Mnesia after finalize (default `false`)
|
||||||
|
* `{mnesia_dir, Dir}` — override target Mnesia directory
|
||||||
|
* `{skip_rocksdb, true}` / `{skip_mnesia, true}` — partial restore
|
||||||
|
|
||||||
#### <a name="Caveats">Caveats</a> ####
|
#### <a name="Caveats">Caveats</a> ####
|
||||||
|
|
||||||
Avoid placing `bag` tables in RocksDB. Although they work, each write
|
Avoid placing `bag` tables in RocksDB. Although they work, each write
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ is provided.
|
|||||||
|
|
||||||
* sext (included as dependency)
|
* sext (included as dependency)
|
||||||
|
|
||||||
* Erlang/OTP 21.0 or newer (https://github.com/erlang/otp)
|
* Erlang/OTP 27.0 or newer (https://github.com/erlang/otp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ backs up data.
|
|||||||
|
|
||||||
- rocksdb (included as dependency)
|
- rocksdb (included as dependency)
|
||||||
- sext (included as dependency)
|
- sext (included as dependency)
|
||||||
- Erlang/OTP 22.0 or newer (https://github.com/erlang/otp)
|
- Erlang/OTP 27.0 or newer (https://github.com/erlang/otp)
|
||||||
|
|
||||||
### Getting started
|
### Getting started
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
%% -*- erlang -*-
|
%% -*- erlang -*-
|
||||||
{minimum_otp_vsn, "21.0"}.
|
{minimum_otp_vsn, "27.0"}.
|
||||||
|
|
||||||
{deps,
|
{deps,
|
||||||
[
|
[
|
||||||
|
|||||||
+43
-1
@@ -49,7 +49,11 @@
|
|||||||
|
|
||||||
-export([register/0,
|
-export([register/0,
|
||||||
register/1,
|
register/1,
|
||||||
default_alias/0]).
|
default_alias/0,
|
||||||
|
backup/1,
|
||||||
|
backup/2,
|
||||||
|
restore/1,
|
||||||
|
restore/2]).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%% DEBUG API
|
%% DEBUG API
|
||||||
@@ -218,6 +222,44 @@ get_ref(Tab) ->
|
|||||||
default_alias() ->
|
default_alias() ->
|
||||||
rocksdb_copies.
|
rocksdb_copies.
|
||||||
|
|
||||||
|
%% @doc Hybrid local backup of Mnesia + RocksDB tables.
|
||||||
|
%%
|
||||||
|
%% Similar intent to `mnesia:backup/1`, but RocksDB-backed tables are backed up
|
||||||
|
%% with the RocksDB backup engine instead of record-level iteration.
|
||||||
|
%%
|
||||||
|
%% `Dir` must be a non-existent or empty directory. On success it contains:
|
||||||
|
%% - `MANIFEST` — restore metadata
|
||||||
|
%% - `mnesia.BUP` — schema and non-RocksDB tables
|
||||||
|
%% - `rocksdb/...` — native RocksDB backups for each open DB
|
||||||
|
%%
|
||||||
|
%% Local node only. See `mnesia_rocksdb_admin:backup/2` for details and options.
|
||||||
|
%% @end
|
||||||
|
-spec backup(file:filename_all()) -> ok | {error, term()}.
|
||||||
|
backup(Dir) ->
|
||||||
|
mnesia_rocksdb_admin:backup(Dir, #{}).
|
||||||
|
|
||||||
|
%% @equiv mnesia_rocksdb_admin:backup/2
|
||||||
|
-spec backup(file:filename_all(), map()) -> ok | {error, term()}.
|
||||||
|
backup(Dir, Opts) when is_map(Opts) ->
|
||||||
|
mnesia_rocksdb_admin:backup(Dir, Opts).
|
||||||
|
|
||||||
|
%% @doc Restore a hybrid backup created by {@link backup/1}.
|
||||||
|
%%
|
||||||
|
%% Offline restore: RocksDB data is restored into a sibling staging directory
|
||||||
|
%% first; Mnesia fallback runs with mountpoints redirected there (so the
|
||||||
|
%% fallback purge does not wipe RocksDB data); then staged dirs are moved into
|
||||||
|
%% the real Mnesia directory. Optionally starts Mnesia (`#{start => true}`).
|
||||||
|
%%
|
||||||
|
%% See `mnesia_rocksdb_admin:restore/2` for options and the full sequence.
|
||||||
|
%% @end
|
||||||
|
-spec restore(file:filename_all()) -> ok | {error, term()}.
|
||||||
|
restore(Dir) ->
|
||||||
|
mnesia_rocksdb_admin:restore(Dir, #{}).
|
||||||
|
|
||||||
|
%% @equiv mnesia_rocksdb_admin:restore/2
|
||||||
|
-spec restore(file:filename_all(), map()) -> ok | {error, term()}.
|
||||||
|
restore(Dir, Opts) when is_map(Opts) ->
|
||||||
|
mnesia_rocksdb_admin:restore(Dir, Opts).
|
||||||
|
|
||||||
encode_key(Key) ->
|
encode_key(Key) ->
|
||||||
mnesia_rocksdb_lib:encode_key(Key, sext).
|
mnesia_rocksdb_lib:encode_key(Key, sext).
|
||||||
|
|||||||
+826
-17
@@ -17,6 +17,11 @@
|
|||||||
, request_ref/2 %% (Alias, Name) -> {ok, Ref} | {error, _}
|
, request_ref/2 %% (Alias, Name) -> {ok, Ref} | {error, _}
|
||||||
, close_table/2
|
, close_table/2
|
||||||
, clear_table/1
|
, clear_table/1
|
||||||
|
, backup/1 %% (Dir) -> ok | {error, _}
|
||||||
|
, backup/2 %% (Dir, Opts) -> ok | {error, _}
|
||||||
|
, restore/1 %% (Dir) -> ok | {error, _}
|
||||||
|
, restore/2 %% (Dir, Opts) -> ok | {error, _}
|
||||||
|
, list_open_dbs/0 %% () -> [db_backup_spec()]
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([ migrate_standalone/2
|
-export([ migrate_standalone/2
|
||||||
@@ -109,7 +114,35 @@
|
|||||||
| {abort_migration, tabname()}
|
| {abort_migration, tabname()}
|
||||||
| {prep_close, table()}
|
| {prep_close, table()}
|
||||||
| {close_table, table()}
|
| {close_table, table()}
|
||||||
| {clear_table, table() | cf() }.
|
| {clear_table, table() | cf() }
|
||||||
|
| list_open_dbs.
|
||||||
|
|
||||||
|
%% Spec describing an open RocksDB instance eligible for backup.
|
||||||
|
-type db_backup_spec() :: #{ kind := admin | standalone
|
||||||
|
, alias := alias()
|
||||||
|
, name := table()
|
||||||
|
, db_ref := db_ref()
|
||||||
|
, mountpoint := file:filename()
|
||||||
|
}.
|
||||||
|
|
||||||
|
-type backup_opts() :: #{ verify => boolean()
|
||||||
|
, mnesia_backup_module => module()
|
||||||
|
}.
|
||||||
|
|
||||||
|
%% Restore options:
|
||||||
|
%% - `mnesia_dir': target Mnesia directory (default: configured Mnesia dir)
|
||||||
|
%% - `stop': stop Mnesia if running before restore (default true)
|
||||||
|
%% - `start': start Mnesia after restore (default false)
|
||||||
|
%% - `mnesia_backup_module': module for reading mnesia.BUP
|
||||||
|
%% - `skip_rocksdb': only install Mnesia fallback (default false)
|
||||||
|
%% - `skip_mnesia': only restore RocksDB DBs (default false)
|
||||||
|
-type restore_opts() :: #{ mnesia_dir => file:filename_all()
|
||||||
|
, stop => boolean()
|
||||||
|
, start => boolean()
|
||||||
|
, mnesia_backup_module => module()
|
||||||
|
, skip_rocksdb => boolean()
|
||||||
|
, skip_mnesia => boolean()
|
||||||
|
}.
|
||||||
|
|
||||||
-type reason() :: any().
|
-type reason() :: any().
|
||||||
-type reply() :: any().
|
-type reply() :: any().
|
||||||
@@ -164,7 +197,13 @@ erase_pt_list(Names) ->
|
|||||||
|
|
||||||
check_application_defaults(Meta) ->
|
check_application_defaults(Meta) ->
|
||||||
Value = application:get_env(mnesia_rocksdb, mnesia_compatible_aborts, false),
|
Value = application:get_env(mnesia_rocksdb, mnesia_compatible_aborts, false),
|
||||||
Meta#{ {mnesia_compatible_aborts} => Value }.
|
Meta1 = Meta#{ {mnesia_compatible_aborts} => Value },
|
||||||
|
case application:get_env(mnesia_rocksdb, restore_mountpoint_root) of
|
||||||
|
{ok, Root} when is_list(Root); is_binary(Root) ->
|
||||||
|
Meta1#{ {restore_mountpoint_root} => filename:absname(Root) };
|
||||||
|
_ ->
|
||||||
|
Meta1
|
||||||
|
end.
|
||||||
|
|
||||||
get_cached_env(Key, Default) ->
|
get_cached_env(Key, Default) ->
|
||||||
maps:get({Key}, meta(), Default).
|
maps:get({Key}, meta(), Default).
|
||||||
@@ -200,6 +239,96 @@ get_pt(Name, Default) ->
|
|||||||
set_and_cache_env(Key, Value) ->
|
set_and_cache_env(Key, Value) ->
|
||||||
gen_server:call(?MODULE, {set_and_cache_env, Key, Value}).
|
gen_server:call(?MODULE, {set_and_cache_env, Key, Value}).
|
||||||
|
|
||||||
|
%% @doc Hybrid backup of Mnesia metadata/non-RocksDB tables and RocksDB DBs.
|
||||||
|
%%
|
||||||
|
%% Creates a directory layout:
|
||||||
|
%% ```
|
||||||
|
%% Dir/
|
||||||
|
%% MANIFEST
|
||||||
|
%% mnesia.BUP
|
||||||
|
%% rocksdb/<alias>/admin/
|
||||||
|
%% rocksdb/standalone/<name>/
|
||||||
|
%% '''
|
||||||
|
%%
|
||||||
|
%% Steps:
|
||||||
|
%% 1. Activate a Mnesia checkpoint covering schema and non-RocksDB tables only
|
||||||
|
%% (avoids heavy RocksDB retainer tables).
|
||||||
|
%% 2. For each open RocksDB instance (admin DBs and standalone tables), create a
|
||||||
|
%% RocksDB-native backup via the backup engine.
|
||||||
|
%% 3. Dump the checkpoint with `mnesia:backup_checkpoint/3`, excluding RocksDB
|
||||||
|
%% table data (schema still describes those tables).
|
||||||
|
%% 4. Write a MANIFEST describing the layout for a future restore.
|
||||||
|
%%
|
||||||
|
%% Local-node only. Cross-store consistency is best-effort within the checkpoint
|
||||||
|
%% window.
|
||||||
|
%%
|
||||||
|
%% Options:
|
||||||
|
%% - `verify` (boolean, default false): call `rocksdb:verify_backup/2`
|
||||||
|
%% - `mnesia_backup_module` (module): override Mnesia backup media module
|
||||||
|
-spec backup(file:filename_all()) -> ok | {error, term()}.
|
||||||
|
backup(Dir) ->
|
||||||
|
backup(Dir, #{}).
|
||||||
|
|
||||||
|
-spec backup(file:filename_all(), backup_opts()) -> ok | {error, term()}.
|
||||||
|
backup(Dir, Opts) when is_map(Opts) ->
|
||||||
|
ensure_started(),
|
||||||
|
case mnesia:system_info(is_running) of
|
||||||
|
yes ->
|
||||||
|
case prepare_backup_dir(Dir) of
|
||||||
|
ok ->
|
||||||
|
try do_backup(filename:absname(Dir), Opts)
|
||||||
|
catch
|
||||||
|
error:Reason:Stack ->
|
||||||
|
{error, {Reason, Stack}};
|
||||||
|
throw:{error, _} = Err ->
|
||||||
|
Err
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
Other ->
|
||||||
|
{error, {mnesia_not_running, Other}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Restore a hybrid backup previously created by {@link backup/1}.
|
||||||
|
%%
|
||||||
|
%% Offline restore (local node). Mnesia fallback application **purges** the
|
||||||
|
%% Mnesia directory, so RocksDB data is restored first into a **sibling**
|
||||||
|
%% staging directory and mountpoints are redirected while the fallback runs:
|
||||||
|
%%
|
||||||
|
%% 1. Stop Mnesia (closes RocksDB handles).
|
||||||
|
%% 2. Restore each RocksDB DB into
|
||||||
|
%% `"<mnesia_dir_basename>.rocksdb_restore/"` (sibling of the Mnesia dir).
|
||||||
|
%% 3. Set `restore_mountpoint_root` so `data_mountpoint/1` uses the staging
|
||||||
|
%% location.
|
||||||
|
%% 4. Install `mnesia.BUP` and start Mnesia (fallback purges the real Mnesia
|
||||||
|
%% dir; RocksDB tables open from staging with full data).
|
||||||
|
%% 5. Stop Mnesia, clear the redirect, move staged `*.extrdb` into the real
|
||||||
|
%% Mnesia directory.
|
||||||
|
%% 6. Optionally start Mnesia (`#{start => true}`).
|
||||||
|
-spec restore(file:filename_all()) -> ok | {error, term()}.
|
||||||
|
restore(Dir) ->
|
||||||
|
restore(Dir, #{}).
|
||||||
|
|
||||||
|
-spec restore(file:filename_all(), restore_opts()) -> ok | {error, term()}.
|
||||||
|
restore(Dir, Opts) when is_map(Opts) ->
|
||||||
|
try do_restore(filename:absname(Dir), Opts)
|
||||||
|
catch
|
||||||
|
error:Reason:Stack ->
|
||||||
|
{error, {Reason, Stack}};
|
||||||
|
throw:{error, _} = Err ->
|
||||||
|
Err
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Snapshot of open RocksDB instances (admin + standalone).
|
||||||
|
-spec list_open_dbs() -> [db_backup_spec()].
|
||||||
|
list_open_dbs() ->
|
||||||
|
ensure_started(),
|
||||||
|
case gen_server:call(?MODULE, {[], list_open_dbs}, infinity) of
|
||||||
|
{ok, Dbs} -> Dbs;
|
||||||
|
{error, _} = E -> error(E)
|
||||||
|
end.
|
||||||
|
|
||||||
create_table(Alias, Name, Props) ->
|
create_table(Alias, Name, Props) ->
|
||||||
call(Alias, {create_table, Name, Props}).
|
call(Alias, {create_table, Name, Props}).
|
||||||
|
|
||||||
@@ -616,6 +745,8 @@ handle_call({[], {get_ref, Name}}, _From, St) ->
|
|||||||
_ ->
|
_ ->
|
||||||
{reply, {error, not_found}, St}
|
{reply, {error, not_found}, St}
|
||||||
end;
|
end;
|
||||||
|
handle_call({[], list_open_dbs}, _From, St) ->
|
||||||
|
{reply, {ok, list_open_dbs_from_st(St)}, St};
|
||||||
handle_call({Alias, Req}, _From, St) ->
|
handle_call({Alias, Req}, _From, St) ->
|
||||||
handle_call_for_alias(Alias, Req, St);
|
handle_call_for_alias(Alias, Req, St);
|
||||||
handle_call(_Req, _From, St) ->
|
handle_call(_Req, _From, St) ->
|
||||||
@@ -1604,23 +1735,35 @@ do_close_table(Alias, Name, Backend, #st{standalone = Ts} = St) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
do_delete_table(Alias, Name, Backend, #st{} = St) ->
|
do_delete_table(Alias, Name, Backend, #st{} = St) ->
|
||||||
case find_cf(Alias, Name, Backend, St) of
|
case restore_staging_active() of
|
||||||
{ok, Where} ->
|
true ->
|
||||||
erase_pt(Name),
|
%% Hybrid restore: RocksDB lives in a staging sibling. Mnesia
|
||||||
case Where of
|
%% fallback calls delete_table before reopening; must not destroy
|
||||||
#{db_ref := DbRef, cf_handle := CfH, type := column_family} ->
|
%% staged files or drop CFs that still hold restored data.
|
||||||
rocksdb:drop_column_family(DbRef, CfH),
|
{ok, St};
|
||||||
drop_cached_cf(tab_to_cf_name(Name), CfH),
|
false ->
|
||||||
rocksdb:destroy_column_family(DbRef, CfH),
|
case find_cf(Alias, Name, Backend, St) of
|
||||||
{ok, delete_cf(Alias, Name, St)};
|
{ok, Where} ->
|
||||||
#{type := standalone} = R ->
|
erase_pt(Name),
|
||||||
St1 = close_and_delete_standalone(R, St),
|
case Where of
|
||||||
{ok, St1}
|
#{db_ref := DbRef, cf_handle := CfH,
|
||||||
end;
|
type := column_family} ->
|
||||||
error ->
|
rocksdb:drop_column_family(DbRef, CfH),
|
||||||
{error, not_found}
|
drop_cached_cf(tab_to_cf_name(Name), CfH),
|
||||||
|
rocksdb:destroy_column_family(DbRef, CfH),
|
||||||
|
{ok, delete_cf(Alias, Name, St)};
|
||||||
|
#{type := standalone} = R ->
|
||||||
|
St1 = close_and_delete_standalone(R, St),
|
||||||
|
{ok, St1}
|
||||||
|
end;
|
||||||
|
error ->
|
||||||
|
{error, not_found}
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
restore_staging_active() ->
|
||||||
|
mnesia_rocksdb_lib:restore_mountpoint_root() =/= undefined.
|
||||||
|
|
||||||
load_admin_db(Alias, Opts) ->
|
load_admin_db(Alias, Opts) ->
|
||||||
DbName = {admin, Alias},
|
DbName = {admin, Alias},
|
||||||
open_db(DbName, Alias, Opts, [DbName], true).
|
open_db(DbName, Alias, Opts, [DbName], true).
|
||||||
@@ -2377,3 +2520,669 @@ destroy_db(MPd, Opts, RetriesLeft, _) ->
|
|||||||
|
|
||||||
get_retries() -> 30.
|
get_retries() -> 30.
|
||||||
get_retry_delay() -> 10000.
|
get_retry_delay() -> 10000.
|
||||||
|
|
||||||
|
%% ===================================================================
|
||||||
|
%% Hybrid backup (Mnesia checkpoint + RocksDB backup engine)
|
||||||
|
%% ===================================================================
|
||||||
|
|
||||||
|
list_open_dbs_from_st(#st{backends = Bs, standalone = Ts}) ->
|
||||||
|
Admin =
|
||||||
|
maps:fold(
|
||||||
|
fun(Alias, #{db_ref := DbRef, mountpoint := MP}, Acc) ->
|
||||||
|
[#{ kind => admin
|
||||||
|
, alias => Alias
|
||||||
|
, name => {admin, Alias}
|
||||||
|
, db_ref => DbRef
|
||||||
|
, mountpoint => MP } | Acc];
|
||||||
|
(_, _, Acc) ->
|
||||||
|
Acc
|
||||||
|
end, [], Bs),
|
||||||
|
Standalone =
|
||||||
|
maps:fold(
|
||||||
|
fun({Alias, Name}, #{db_ref := DbRef, mountpoint := MP} = R, Acc) ->
|
||||||
|
case maps:get(status, R, open) of
|
||||||
|
closed ->
|
||||||
|
Acc;
|
||||||
|
_ ->
|
||||||
|
[#{ kind => standalone
|
||||||
|
, alias => Alias
|
||||||
|
, name => Name
|
||||||
|
, db_ref => DbRef
|
||||||
|
, mountpoint => MP } | Acc]
|
||||||
|
end;
|
||||||
|
(_, _, Acc) ->
|
||||||
|
Acc
|
||||||
|
end, [], Ts),
|
||||||
|
Admin ++ Standalone.
|
||||||
|
|
||||||
|
prepare_backup_dir(Dir) ->
|
||||||
|
case filelib:is_dir(Dir) of
|
||||||
|
true ->
|
||||||
|
case file:list_dir(Dir) of
|
||||||
|
{ok, []} ->
|
||||||
|
ok;
|
||||||
|
{ok, _} ->
|
||||||
|
{error, {not_empty, Dir}};
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
case filelib:ensure_dir(filename:join(Dir, "dummy")) of
|
||||||
|
ok ->
|
||||||
|
case file:make_dir(Dir) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, eexist} ->
|
||||||
|
%% Race: created between is_dir and make_dir
|
||||||
|
prepare_backup_dir(Dir);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_backup(Dir, Opts) ->
|
||||||
|
{RdbTabs, NonRdbTabs} = classify_tables(),
|
||||||
|
CpArgs = [ {ram_overrides_dump, false}
|
||||||
|
, {max, NonRdbTabs}
|
||||||
|
, {allow_remote, false} ],
|
||||||
|
case mnesia:activate_checkpoint(CpArgs) of
|
||||||
|
{ok, CpName, _Nodes} ->
|
||||||
|
try
|
||||||
|
Dbs = list_open_dbs(),
|
||||||
|
case backup_rocksdbs(Dir, Dbs, Opts) of
|
||||||
|
{ok, DbInfos} ->
|
||||||
|
Bup = filename:join(Dir, "mnesia.BUP"),
|
||||||
|
case backup_mnesia_checkpoint(CpName, Bup, NonRdbTabs, Opts) of
|
||||||
|
ok ->
|
||||||
|
write_manifest(Dir, NonRdbTabs, RdbTabs, DbInfos);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end
|
||||||
|
after
|
||||||
|
_ = mnesia:deactivate_checkpoint(CpName)
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
classify_tables() ->
|
||||||
|
All = mnesia:system_info(tables),
|
||||||
|
Rdb = [T || T <- All, is_local_rdb_table(T)],
|
||||||
|
NonRdb = All -- Rdb,
|
||||||
|
{lists:sort(Rdb), lists:sort(NonRdb)}.
|
||||||
|
|
||||||
|
is_local_rdb_table(schema) ->
|
||||||
|
false;
|
||||||
|
is_local_rdb_table(Tab) ->
|
||||||
|
try mnesia:table_info(Tab, storage_type) of
|
||||||
|
{ext, _Alias, mnesia_rocksdb} -> true;
|
||||||
|
_ -> false
|
||||||
|
catch
|
||||||
|
_:_ -> false
|
||||||
|
end.
|
||||||
|
|
||||||
|
backup_mnesia_checkpoint(CpName, BupPath, NonRdbTabs, Opts) ->
|
||||||
|
Mod = maps:get(mnesia_backup_module, Opts,
|
||||||
|
mnesia:system_info(backup_module)),
|
||||||
|
%% mnesia:backup_checkpoint/3 is typed as module()-only; the implementation
|
||||||
|
%% (and mnesia_log:backup_checkpoint/3) accepts a proplist with table filter.
|
||||||
|
Args = [ {module, Mod}
|
||||||
|
, {tables, NonRdbTabs}
|
||||||
|
, {scope, local} ],
|
||||||
|
case mnesia_log:backup_checkpoint(CpName, BupPath, Args) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end.
|
||||||
|
|
||||||
|
backup_rocksdbs(Dir, Dbs, Opts) ->
|
||||||
|
backup_rocksdbs(Dir, Dbs, Opts, []).
|
||||||
|
|
||||||
|
backup_rocksdbs(_Dir, [], _Opts, Acc) ->
|
||||||
|
{ok, lists:reverse(Acc)};
|
||||||
|
backup_rocksdbs(Dir, [Spec | Rest], Opts, Acc) ->
|
||||||
|
case backup_one_db(Dir, Spec, Opts) of
|
||||||
|
{ok, Info} ->
|
||||||
|
backup_rocksdbs(Dir, Rest, Opts, [Info | Acc]);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
backup_one_db(Dir, #{ db_ref := DbRef
|
||||||
|
, mountpoint := MP
|
||||||
|
, kind := Kind
|
||||||
|
, alias := Alias
|
||||||
|
, name := Name } = Spec, Opts) ->
|
||||||
|
Rel = backup_relpath(Spec),
|
||||||
|
Abs = filename:join(Dir, Rel),
|
||||||
|
case ensure_dir_tree(Abs) of
|
||||||
|
ok ->
|
||||||
|
case rocksdb:open_backup_engine(Abs) of
|
||||||
|
{ok, BE} ->
|
||||||
|
try
|
||||||
|
case rocksdb:create_new_backup(BE, DbRef) of
|
||||||
|
ok ->
|
||||||
|
case latest_backup_id(BE) of
|
||||||
|
{ok, BackupId} ->
|
||||||
|
case maybe_verify_backup(BE, BackupId, Opts) of
|
||||||
|
ok ->
|
||||||
|
{ok, #{ kind => Kind
|
||||||
|
, alias => Alias
|
||||||
|
, name => Name
|
||||||
|
, mountpoint => MP
|
||||||
|
, backup_relpath => Rel
|
||||||
|
, backup_id => BackupId }};
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end
|
||||||
|
after
|
||||||
|
_ = rocksdb:close_backup_engine(BE)
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
backup_relpath(#{kind := admin, alias := Alias}) ->
|
||||||
|
filename:join(["rocksdb", atom_to_list(Alias), "admin"]);
|
||||||
|
backup_relpath(#{kind := standalone, name := Name}) ->
|
||||||
|
filename:join(["rocksdb", "standalone", sanitize_backup_name(Name)]).
|
||||||
|
|
||||||
|
sanitize_backup_name(Name) when is_atom(Name) ->
|
||||||
|
atom_to_list(Name);
|
||||||
|
sanitize_backup_name(Name) ->
|
||||||
|
%% Index/retainer or other compound names: reuse mountpoint basename.
|
||||||
|
filename:basename(mnesia_rocksdb_lib:data_mountpoint(Name), ".extrdb").
|
||||||
|
|
||||||
|
ensure_dir_tree(Dir) ->
|
||||||
|
case filelib:is_dir(Dir) of
|
||||||
|
true ->
|
||||||
|
ok;
|
||||||
|
false ->
|
||||||
|
case filelib:ensure_dir(filename:join(Dir, "dummy")) of
|
||||||
|
ok ->
|
||||||
|
case file:make_dir(Dir) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, eexist} -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
latest_backup_id(BE) ->
|
||||||
|
%% Erase rocksdb's incomplete -spec (list of #{id}) so we can handle the
|
||||||
|
%% real NIF shape ({ok, [#{backup_id => ...}]}).
|
||||||
|
Raw = binary_to_term(term_to_binary(rocksdb:get_backup_info(BE))),
|
||||||
|
case normalize_backup_infos(Raw) of
|
||||||
|
[] ->
|
||||||
|
{error, no_backup_info};
|
||||||
|
Infos ->
|
||||||
|
{ok, lists:max([backup_info_id(I) || I <- Infos])}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec normalize_backup_infos(term()) -> [term()].
|
||||||
|
normalize_backup_infos({ok, Infos}) when is_list(Infos) ->
|
||||||
|
Infos;
|
||||||
|
normalize_backup_infos(Infos) when is_list(Infos) ->
|
||||||
|
Infos;
|
||||||
|
normalize_backup_infos(_) ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
-spec backup_info_id(term()) -> non_neg_integer().
|
||||||
|
backup_info_id(Info) when is_map(Info) ->
|
||||||
|
case maps:find(id, Info) of
|
||||||
|
{ok, Id} when is_integer(Id), Id >= 0 ->
|
||||||
|
Id;
|
||||||
|
error ->
|
||||||
|
case maps:find(backup_id, Info) of
|
||||||
|
{ok, Id} when is_integer(Id), Id >= 0 ->
|
||||||
|
Id;
|
||||||
|
_ ->
|
||||||
|
error({missing_backup_id, Info})
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
error({invalid_backup_id, Info})
|
||||||
|
end;
|
||||||
|
backup_info_id(Other) ->
|
||||||
|
error({invalid_backup_info, Other}).
|
||||||
|
|
||||||
|
maybe_verify_backup(BE, BackupId, Opts) ->
|
||||||
|
case maps:get(verify, Opts, false) of
|
||||||
|
true ->
|
||||||
|
rocksdb:verify_backup(BE, BackupId);
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
write_manifest(Dir, NonRdbTabs, RdbTabs, DbInfos) ->
|
||||||
|
Manifest =
|
||||||
|
#{ version => 1
|
||||||
|
, created => erlang:system_time(second)
|
||||||
|
, node => node()
|
||||||
|
, mnesia_directory => mnesia:system_info(directory)
|
||||||
|
, mnesia_backup => "mnesia.BUP"
|
||||||
|
, mnesia_tables => NonRdbTabs
|
||||||
|
, rocksdb_tables => RdbTabs
|
||||||
|
, dbs => [maps:with([kind, alias, name, mountpoint,
|
||||||
|
backup_relpath, backup_id], I)
|
||||||
|
|| I <- DbInfos]
|
||||||
|
},
|
||||||
|
Path = filename:join(Dir, "MANIFEST"),
|
||||||
|
Bin = unicode:characters_to_binary(io_lib:format("~p.~n", [Manifest])),
|
||||||
|
case file:write_file(Path, Bin) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% ===================================================================
|
||||||
|
%% Hybrid restore
|
||||||
|
%% ===================================================================
|
||||||
|
|
||||||
|
do_restore(Dir, Opts) ->
|
||||||
|
case read_manifest(Dir) of
|
||||||
|
{ok, Manifest} ->
|
||||||
|
case maybe_stop_mnesia(Opts) of
|
||||||
|
ok ->
|
||||||
|
restore_body(Dir, Manifest, Opts);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
restore_body(Dir, Manifest, Opts) ->
|
||||||
|
SkipMnesia = maps:get(skip_mnesia, Opts, false),
|
||||||
|
SkipRocks = maps:get(skip_rocksdb, Opts, false),
|
||||||
|
MnesiaDir = target_mnesia_dir(Opts, Manifest),
|
||||||
|
case wait_admin_down(100, 50) of
|
||||||
|
ok ->
|
||||||
|
restore_body1(Dir, Manifest, Opts, MnesiaDir,
|
||||||
|
SkipMnesia, SkipRocks);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
restore_body1(Dir, Manifest, Opts, MnesiaDir, SkipMnesia, SkipRocks) ->
|
||||||
|
case {SkipMnesia, SkipRocks} of
|
||||||
|
{true, true} ->
|
||||||
|
ok;
|
||||||
|
{true, false} ->
|
||||||
|
%% RocksDB only: write straight into final mountpoints.
|
||||||
|
case restore_rocksdbs(Dir, Manifest, MnesiaDir) of
|
||||||
|
ok -> maybe_start_mnesia(Opts);
|
||||||
|
{error, _} = E -> E
|
||||||
|
end;
|
||||||
|
{false, true} ->
|
||||||
|
%% Mnesia fallback only (no RocksDB data restore).
|
||||||
|
case install_and_apply_fallback(Dir, Manifest, Opts) of
|
||||||
|
ok -> maybe_start_mnesia(Opts);
|
||||||
|
{error, _} = E -> E
|
||||||
|
end;
|
||||||
|
{false, false} ->
|
||||||
|
staged_full_restore(Dir, Manifest, Opts, MnesiaDir)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Full hybrid restore via staging sibling (survives fallback purge).
|
||||||
|
staged_full_restore(Dir, Manifest, Opts, MnesiaDir) ->
|
||||||
|
Staging = mnesia_rocksdb_lib:restore_staging_root(MnesiaDir),
|
||||||
|
try
|
||||||
|
case prepare_staging_root(Staging) of
|
||||||
|
ok ->
|
||||||
|
case restore_rocksdbs(Dir, Manifest, Staging) of
|
||||||
|
ok ->
|
||||||
|
set_restore_mountpoint_root(Staging),
|
||||||
|
case install_and_apply_fallback(Dir, Manifest, Opts) of
|
||||||
|
ok ->
|
||||||
|
finalize_staging(Staging, Manifest, MnesiaDir,
|
||||||
|
Opts);
|
||||||
|
{error, _} = E ->
|
||||||
|
clear_restore_mountpoint_root(),
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end
|
||||||
|
after
|
||||||
|
clear_restore_mountpoint_root()
|
||||||
|
end.
|
||||||
|
|
||||||
|
finalize_staging(Staging, Manifest, MnesiaDir, Opts) ->
|
||||||
|
clear_restore_mountpoint_root(),
|
||||||
|
case relocate_staging_to_mnesia(Staging, Manifest, MnesiaDir) of
|
||||||
|
ok ->
|
||||||
|
_ = clear_dir(Staging),
|
||||||
|
maybe_start_mnesia(Opts);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
prepare_staging_root(Staging) ->
|
||||||
|
case clear_dir(Staging) of
|
||||||
|
ok ->
|
||||||
|
ensure_dir_tree(Staging);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
set_restore_mountpoint_root(Root) ->
|
||||||
|
Abs = filename:absname(Root),
|
||||||
|
application:set_env(mnesia_rocksdb, restore_mountpoint_root, Abs),
|
||||||
|
%% Keep PT in sync when present (admin may not be running yet).
|
||||||
|
case persistent_term:get(?PT_KEY, undefined) of
|
||||||
|
undefined ->
|
||||||
|
ok;
|
||||||
|
Meta when is_map(Meta) ->
|
||||||
|
persistent_term:put(
|
||||||
|
?PT_KEY, Meta#{ {restore_mountpoint_root} => Abs })
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
clear_restore_mountpoint_root() ->
|
||||||
|
application:unset_env(mnesia_rocksdb, restore_mountpoint_root),
|
||||||
|
case persistent_term:get(?PT_KEY, undefined) of
|
||||||
|
undefined ->
|
||||||
|
ok;
|
||||||
|
Meta when is_map(Meta) ->
|
||||||
|
persistent_term:put(
|
||||||
|
?PT_KEY, maps:remove({restore_mountpoint_root}, Meta))
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
install_and_apply_fallback(Dir, Manifest, Opts) ->
|
||||||
|
case install_mnesia_fallback(Dir, Manifest, Opts) of
|
||||||
|
ok ->
|
||||||
|
apply_mnesia_fallback();
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Start Mnesia so FALLBACK.BUP is applied (RocksDB opens from staging if set),
|
||||||
|
%% wait for tables, then stop so files can be relocated to the real Mnesia dir.
|
||||||
|
apply_mnesia_fallback() ->
|
||||||
|
StartRes =
|
||||||
|
case mnesia:start() of
|
||||||
|
ok -> ok;
|
||||||
|
{error, {already_started, mnesia}} -> ok;
|
||||||
|
{error, _} = StartErr -> StartErr
|
||||||
|
end,
|
||||||
|
case StartRes of
|
||||||
|
ok ->
|
||||||
|
case mnesia:system_info(is_running) of
|
||||||
|
yes ->
|
||||||
|
Tabs = mnesia:system_info(tables),
|
||||||
|
case mnesia:wait_for_tables(Tabs, 120000) of
|
||||||
|
ok ->
|
||||||
|
stopped = mnesia:stop(),
|
||||||
|
wait_admin_down(100, 50);
|
||||||
|
{timeout, _} = T ->
|
||||||
|
_ = mnesia:stop(),
|
||||||
|
_ = wait_admin_down(100, 50),
|
||||||
|
{error, T};
|
||||||
|
{error, _} = WaitErr ->
|
||||||
|
_ = mnesia:stop(),
|
||||||
|
_ = wait_admin_down(100, 50),
|
||||||
|
WaitErr
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, fallback_apply_failed}
|
||||||
|
end;
|
||||||
|
{error, _} = Err ->
|
||||||
|
Err
|
||||||
|
end.
|
||||||
|
|
||||||
|
relocate_staging_to_mnesia(Staging, #{dbs := Dbs}, MnesiaDir) ->
|
||||||
|
relocate_loop(Dbs, Staging, MnesiaDir).
|
||||||
|
|
||||||
|
relocate_loop([], _Staging, _MnesiaDir) ->
|
||||||
|
ok;
|
||||||
|
relocate_loop([#{mountpoint := OrigMP} | Rest], Staging, MnesiaDir) ->
|
||||||
|
Base = filename:basename(OrigMP),
|
||||||
|
From = filename:join(Staging, Base),
|
||||||
|
To = filename:join(MnesiaDir, Base),
|
||||||
|
case relocate_one_mountpoint(From, To, MnesiaDir) of
|
||||||
|
ok ->
|
||||||
|
relocate_loop(Rest, Staging, MnesiaDir);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
relocate_one_mountpoint(From, To, MnesiaDir) ->
|
||||||
|
case filelib:is_dir(From) of
|
||||||
|
false ->
|
||||||
|
{error, {missing_staged_mountpoint, From}};
|
||||||
|
true ->
|
||||||
|
case is_under_dir(To, MnesiaDir) of
|
||||||
|
false ->
|
||||||
|
{error, {mountpoint_outside_mnesia_dir, To, MnesiaDir}};
|
||||||
|
true ->
|
||||||
|
case clear_dir(To) of
|
||||||
|
ok ->
|
||||||
|
case ensure_parent_dir(To) of
|
||||||
|
ok ->
|
||||||
|
case file:rename(From, To) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_parent_dir(Path) ->
|
||||||
|
filelib:ensure_dir(filename:join(Path, "dummy")).
|
||||||
|
|
||||||
|
read_manifest(Dir) ->
|
||||||
|
Path = filename:join(Dir, "MANIFEST"),
|
||||||
|
case file:consult(Path) of
|
||||||
|
{ok, [#{version := 1} = M]} ->
|
||||||
|
{ok, M};
|
||||||
|
{ok, [Other]} ->
|
||||||
|
{error, {invalid_manifest, Other}};
|
||||||
|
{ok, Other} ->
|
||||||
|
{error, {invalid_manifest, Other}};
|
||||||
|
{error, enoent} ->
|
||||||
|
{error, {missing_manifest, Path}};
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
target_mnesia_dir(Opts, Manifest) ->
|
||||||
|
case maps:get(mnesia_dir, Opts, undefined) of
|
||||||
|
undefined ->
|
||||||
|
case mnesia_directory() of
|
||||||
|
undefined ->
|
||||||
|
maps:get(mnesia_directory, Manifest);
|
||||||
|
Dir ->
|
||||||
|
Dir
|
||||||
|
end;
|
||||||
|
Dir ->
|
||||||
|
filename:absname(Dir)
|
||||||
|
end.
|
||||||
|
|
||||||
|
mnesia_directory() ->
|
||||||
|
try mnesia:system_info(directory) of
|
||||||
|
Dir when is_list(Dir); is_binary(Dir) -> filename:absname(Dir);
|
||||||
|
_ -> undefined
|
||||||
|
catch
|
||||||
|
_:_ ->
|
||||||
|
case application:get_env(mnesia, dir) of
|
||||||
|
{ok, Dir} -> filename:absname(Dir);
|
||||||
|
undefined -> undefined
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
maybe_stop_mnesia(Opts) ->
|
||||||
|
case maps:get(stop, Opts, true) of
|
||||||
|
false ->
|
||||||
|
case mnesia:system_info(is_running) of
|
||||||
|
yes ->
|
||||||
|
{error, {mnesia_running,
|
||||||
|
"stop Mnesia before restore, or pass #{stop => true}"}};
|
||||||
|
_ ->
|
||||||
|
ok
|
||||||
|
end;
|
||||||
|
true ->
|
||||||
|
case mnesia:system_info(is_running) of
|
||||||
|
yes ->
|
||||||
|
stopped = mnesia:stop(),
|
||||||
|
wait_admin_down(100, 50);
|
||||||
|
_ ->
|
||||||
|
%% Best-effort: admin may still hold open DBs if left over.
|
||||||
|
stop_admin_if_alive(),
|
||||||
|
wait_admin_down(100, 50)
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
stop_admin_if_alive() ->
|
||||||
|
case whereis(?MODULE) of
|
||||||
|
undefined ->
|
||||||
|
ok;
|
||||||
|
Pid when is_pid(Pid) ->
|
||||||
|
try gen_server:stop(Pid, shutdown, 30000)
|
||||||
|
catch
|
||||||
|
_:_ -> ok
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
wait_admin_down(0, _Sleep) ->
|
||||||
|
case whereis(?MODULE) of
|
||||||
|
undefined -> ok;
|
||||||
|
Pid -> {error, {admin_still_running, Pid}}
|
||||||
|
end;
|
||||||
|
wait_admin_down(N, Sleep) ->
|
||||||
|
case whereis(?MODULE) of
|
||||||
|
undefined ->
|
||||||
|
ok;
|
||||||
|
_ ->
|
||||||
|
timer:sleep(Sleep),
|
||||||
|
wait_admin_down(N - 1, Sleep)
|
||||||
|
end.
|
||||||
|
|
||||||
|
maybe_start_mnesia(Opts) ->
|
||||||
|
case maps:get(start, Opts, false) of
|
||||||
|
true ->
|
||||||
|
case mnesia:start() of
|
||||||
|
ok -> ok;
|
||||||
|
{error, {already_started, mnesia}} -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Restore RocksDB engines under Root (staging sibling or final Mnesia dir).
|
||||||
|
restore_rocksdbs(Dir, #{dbs := Dbs}, Root) ->
|
||||||
|
restore_rocksdbs_loop(Dir, Dbs, Root).
|
||||||
|
|
||||||
|
restore_rocksdbs_loop(_Dir, [], _Root) ->
|
||||||
|
ok;
|
||||||
|
restore_rocksdbs_loop(Dir, [Spec | Rest], Root) ->
|
||||||
|
case restore_one_db(Dir, Spec, Root) of
|
||||||
|
ok ->
|
||||||
|
restore_rocksdbs_loop(Dir, Rest, Root);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
restore_one_db(Dir, #{ backup_relpath := Rel
|
||||||
|
, backup_id := BackupId
|
||||||
|
, mountpoint := OrigMP }, Root) ->
|
||||||
|
AbsBackup = filename:join(Dir, Rel),
|
||||||
|
TargetMP = remap_mountpoint(OrigMP, Root),
|
||||||
|
case prepare_restore_mountpoint(TargetMP, Root) of
|
||||||
|
ok ->
|
||||||
|
case rocksdb:open_backup_engine(AbsBackup) of
|
||||||
|
{ok, BE} ->
|
||||||
|
try
|
||||||
|
case rocksdb:restore_db_from_backup(
|
||||||
|
BE, BackupId, TargetMP, TargetMP) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end
|
||||||
|
after
|
||||||
|
_ = rocksdb:close_backup_engine(BE)
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Keep only the basename under Root so restores work when the absolute path
|
||||||
|
%% differs from the backup host (and for staging siblings).
|
||||||
|
remap_mountpoint(OrigMP, Root) ->
|
||||||
|
filename:join(Root, filename:basename(OrigMP)).
|
||||||
|
|
||||||
|
prepare_restore_mountpoint(TargetMP, Root) ->
|
||||||
|
case is_under_dir(TargetMP, Root) of
|
||||||
|
true ->
|
||||||
|
case clear_dir(TargetMP) of
|
||||||
|
ok ->
|
||||||
|
ensure_dir_tree(TargetMP);
|
||||||
|
{error, _} = E ->
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
{error, {mountpoint_outside_root, TargetMP, Root}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_under_dir(Path, Root) ->
|
||||||
|
AbsPath = filename:absname(Path),
|
||||||
|
AbsRoot = filename:absname(Root),
|
||||||
|
lists:prefix(AbsRoot ++ "/", AbsPath ++ "/")
|
||||||
|
orelse AbsPath =:= AbsRoot.
|
||||||
|
|
||||||
|
clear_dir(Dir) ->
|
||||||
|
case filelib:is_dir(Dir) of
|
||||||
|
true ->
|
||||||
|
file:del_dir_r(Dir);
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
install_mnesia_fallback(Dir, Manifest, Opts) ->
|
||||||
|
Rel = maps:get(mnesia_backup, Manifest, "mnesia.BUP"),
|
||||||
|
Bup = filename:join(Dir, Rel),
|
||||||
|
case filelib:is_regular(Bup) of
|
||||||
|
false ->
|
||||||
|
{error, {missing_mnesia_backup, Bup}};
|
||||||
|
true ->
|
||||||
|
Mod = maps:get(mnesia_backup_module, Opts,
|
||||||
|
default_backup_module()),
|
||||||
|
Args = [ {module, Mod}
|
||||||
|
, {scope, local}
|
||||||
|
],
|
||||||
|
case mnesia:install_fallback(Bup, Args) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, _} = E -> E
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
default_backup_module() ->
|
||||||
|
try mnesia:system_info(backup_module)
|
||||||
|
catch
|
||||||
|
_:_ -> mnesia_backup
|
||||||
|
end.
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
, data_mountpoint/1
|
, data_mountpoint/1
|
||||||
, create_mountpoint/1
|
, create_mountpoint/1
|
||||||
, tabname/1
|
, tabname/1
|
||||||
|
, restore_mountpoint_root/0
|
||||||
|
, restore_staging_root/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([ default_encoding/3
|
-export([ default_encoding/3
|
||||||
@@ -59,8 +61,16 @@ create_mountpoint(Tab) ->
|
|||||||
file:make_dir(MPd),
|
file:make_dir(MPd),
|
||||||
ok;
|
ok;
|
||||||
true ->
|
true ->
|
||||||
Dir = mnesia_lib:dir(),
|
AllowedRoots =
|
||||||
case lists:prefix(Dir, MPd) of
|
case restore_mountpoint_root() of
|
||||||
|
undefined -> [mnesia_lib:dir()];
|
||||||
|
Staging -> [mnesia_lib:dir(), Staging]
|
||||||
|
end,
|
||||||
|
case lists:any(
|
||||||
|
fun(Root) ->
|
||||||
|
lists:prefix(filename:absname(Root),
|
||||||
|
filename:absname(MPd))
|
||||||
|
end, AllowedRoots) of
|
||||||
true ->
|
true ->
|
||||||
ok;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
@@ -68,9 +78,37 @@ create_mountpoint(Tab) ->
|
|||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% @doc Absolute RocksDB directory for a logical table / admin DB.
|
||||||
|
%%
|
||||||
|
%% During hybrid restore, `application:get_env(mnesia_rocksdb,
|
||||||
|
%% restore_mountpoint_root)` may point at a sibling staging directory so
|
||||||
|
%% RocksDB data survives Mnesia's fallback purge of the real Mnesia dir.
|
||||||
|
-spec data_mountpoint(term()) -> file:filename().
|
||||||
data_mountpoint(Tab) ->
|
data_mountpoint(Tab) ->
|
||||||
Dir = mnesia_monitor:get_env(dir),
|
Base = tabname(Tab) ++ ".extrdb",
|
||||||
filename:join(Dir, tabname(Tab) ++ ".extrdb").
|
case restore_mountpoint_root() of
|
||||||
|
undefined ->
|
||||||
|
filename:join(mnesia_monitor:get_env(dir), Base);
|
||||||
|
Root ->
|
||||||
|
filename:join(Root, Base)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Staging root used while applying a hybrid restore, if any.
|
||||||
|
-spec restore_mountpoint_root() -> file:filename() | undefined.
|
||||||
|
restore_mountpoint_root() ->
|
||||||
|
case application:get_env(mnesia_rocksdb, restore_mountpoint_root) of
|
||||||
|
{ok, Root} when is_list(Root); is_binary(Root) ->
|
||||||
|
filename:absname(Root);
|
||||||
|
_ ->
|
||||||
|
undefined
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Sibling of the Mnesia directory for staged RocksDB restore data.
|
||||||
|
-spec restore_staging_root(file:filename()) -> file:filename().
|
||||||
|
restore_staging_root(MnesiaDir) ->
|
||||||
|
Abs = filename:absname(MnesiaDir),
|
||||||
|
filename:join(filename:dirname(Abs),
|
||||||
|
filename:basename(Abs) ++ ".rocksdb_restore").
|
||||||
|
|
||||||
tabname({admin, Alias}) ->
|
tabname({admin, Alias}) ->
|
||||||
"mnesia_rocksdb-" ++ atom_to_list(Alias) ++ "-_db";
|
"mnesia_rocksdb-" ++ atom_to_list(Alias) ++ "-_db";
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*-
|
||||||
|
-module(mnesia_rocksdb_backup_SUITE).
|
||||||
|
|
||||||
|
-export([ all/0
|
||||||
|
, suite/0
|
||||||
|
, groups/0
|
||||||
|
, init_per_suite/1
|
||||||
|
, end_per_suite/1
|
||||||
|
, init_per_group/2
|
||||||
|
, end_per_group/2
|
||||||
|
, init_per_testcase/2
|
||||||
|
, end_per_testcase/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ hybrid_backup_cf_and_disc/1
|
||||||
|
, hybrid_backup_with_standalone/1
|
||||||
|
, backup_rejects_nonempty_dir/1
|
||||||
|
, backup_with_verify/1
|
||||||
|
, restore_roundtrip_cf_and_disc/1
|
||||||
|
, restore_roundtrip_with_standalone/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
suite() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[{group, all_tests}].
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[{all_tests, [sequence],
|
||||||
|
[ hybrid_backup_cf_and_disc
|
||||||
|
, hybrid_backup_with_standalone
|
||||||
|
, backup_rejects_nonempty_dir
|
||||||
|
, backup_with_verify
|
||||||
|
, restore_roundtrip_cf_and_disc
|
||||||
|
, restore_roundtrip_with_standalone
|
||||||
|
]}].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_per_group(_, Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_group(_, _Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_per_testcase(_TC, Config) ->
|
||||||
|
mnesia:stop(),
|
||||||
|
ok = mnesia_rocksdb_tlib:start_mnesia(reset),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_testcase(_TC, _Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Tests
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% disc_copies (not ram_copies): with ram_overrides_dump=false, Mnesia
|
||||||
|
%% checkpoints prefer the dumped image for ram_copies, so pure RAM writes
|
||||||
|
%% are a poor stand-in for "persistent non-RocksDB tables".
|
||||||
|
hybrid_backup_cf_and_disc(Config) ->
|
||||||
|
create_rdb_table(t, []),
|
||||||
|
{atomic, ok} =
|
||||||
|
mnesia:create_table(d, [{disc_copies, [node()]},
|
||||||
|
{attributes, [k, v]}]),
|
||||||
|
ok = mnesia:wait_for_tables([t, d], 5000),
|
||||||
|
|
||||||
|
[ok = mnesia:dirty_write({t, K, V}) || {K, V} <- [{a, 1}, {b, 2}, {c, 3}]],
|
||||||
|
[ok = mnesia:dirty_write({d, K, V}) || {K, V} <- [{x, 10}, {y, 20}]],
|
||||||
|
|
||||||
|
BackupDir = backup_dir(Config, "cf_disc"),
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:backup(BackupDir)),
|
||||||
|
|
||||||
|
assert_layout(BackupDir),
|
||||||
|
Manifest = read_manifest(BackupDir),
|
||||||
|
ct:log("MANIFEST = ~p", [Manifest]),
|
||||||
|
|
||||||
|
?assertEqual(1, maps:get(version, Manifest)),
|
||||||
|
?assert(lists:member(t, maps:get(rocksdb_tables, Manifest))),
|
||||||
|
?assertNot(lists:member(t, maps:get(mnesia_tables, Manifest))),
|
||||||
|
?assert(lists:member(schema, maps:get(mnesia_tables, Manifest))),
|
||||||
|
?assert(lists:member(d, maps:get(mnesia_tables, Manifest))),
|
||||||
|
|
||||||
|
Dbs = maps:get(dbs, Manifest),
|
||||||
|
?assert(lists:any(fun(#{kind := admin}) -> true; (_) -> false end, Dbs)),
|
||||||
|
|
||||||
|
assert_admin_backup_ok(BackupDir, Dbs),
|
||||||
|
assert_mnesia_bup_has_table(BackupDir, d),
|
||||||
|
assert_mnesia_bup_lacks_table_data(BackupDir, t),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
hybrid_backup_with_standalone(Config) ->
|
||||||
|
create_rdb_table(t, []),
|
||||||
|
create_rdb_table(ts, [{user_properties, [{rocksdb_standalone, true}]}]),
|
||||||
|
{atomic, ok} =
|
||||||
|
mnesia:create_table(d, [{disc_copies, [node()]},
|
||||||
|
{attributes, [k, v]}]),
|
||||||
|
ok = mnesia:wait_for_tables([t, ts, d], 5000),
|
||||||
|
|
||||||
|
ok = mnesia:dirty_write({t, 1, one}),
|
||||||
|
ok = mnesia:dirty_write({ts, 2, two}),
|
||||||
|
ok = mnesia:dirty_write({d, 3, three}),
|
||||||
|
|
||||||
|
#{type := standalone} = mrdb:get_ref(ts),
|
||||||
|
|
||||||
|
BackupDir = backup_dir(Config, "standalone"),
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:backup(BackupDir)),
|
||||||
|
|
||||||
|
Manifest = read_manifest(BackupDir),
|
||||||
|
RdbTabs = maps:get(rocksdb_tables, Manifest),
|
||||||
|
?assert(lists:member(t, RdbTabs)),
|
||||||
|
?assert(lists:member(ts, RdbTabs)),
|
||||||
|
?assert(lists:member(d, maps:get(mnesia_tables, Manifest))),
|
||||||
|
|
||||||
|
Dbs = maps:get(dbs, Manifest),
|
||||||
|
?assert(lists:any(fun(#{kind := standalone, name := ts}) -> true;
|
||||||
|
(_) -> false end, Dbs)),
|
||||||
|
?assert(lists:any(fun(#{kind := admin}) -> true; (_) -> false end, Dbs)),
|
||||||
|
|
||||||
|
lists:foreach(
|
||||||
|
fun(#{backup_relpath := Rel, backup_id := Id}) ->
|
||||||
|
Abs = filename:join(BackupDir, Rel),
|
||||||
|
{ok, BE} = rocksdb:open_backup_engine(Abs),
|
||||||
|
try
|
||||||
|
?assertEqual(ok, rocksdb:verify_backup(BE, Id))
|
||||||
|
after
|
||||||
|
rocksdb:close_backup_engine(BE)
|
||||||
|
end
|
||||||
|
end, Dbs),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
backup_rejects_nonempty_dir(Config) ->
|
||||||
|
create_rdb_table(t, []),
|
||||||
|
ok = mnesia:wait_for_tables([t], 5000),
|
||||||
|
ok = mnesia:dirty_write({t, 1, a}),
|
||||||
|
|
||||||
|
BackupDir = backup_dir(Config, "nonempty"),
|
||||||
|
ok = filelib:ensure_dir(filename:join(BackupDir, "noise")),
|
||||||
|
case file:make_dir(BackupDir) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, eexist} -> ok
|
||||||
|
end,
|
||||||
|
ok = file:write_file(filename:join(BackupDir, "noise"), <<"x">>),
|
||||||
|
|
||||||
|
?assertMatch({error, {not_empty, _}}, mnesia_rocksdb:backup(BackupDir)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
backup_with_verify(Config) ->
|
||||||
|
create_rdb_table(t, []),
|
||||||
|
ok = mnesia:wait_for_tables([t], 5000),
|
||||||
|
ok = mnesia:dirty_write({t, 1, a}),
|
||||||
|
|
||||||
|
BackupDir = backup_dir(Config, "verify"),
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:backup(BackupDir, #{verify => true})),
|
||||||
|
assert_layout(BackupDir),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
restore_roundtrip_cf_and_disc(Config) ->
|
||||||
|
create_rdb_table(t, []),
|
||||||
|
{atomic, ok} =
|
||||||
|
mnesia:create_table(d, [{disc_copies, [node()]},
|
||||||
|
{attributes, [k, v]}]),
|
||||||
|
ok = mnesia:wait_for_tables([t, d], 5000),
|
||||||
|
|
||||||
|
[ok = mnesia:dirty_write({t, K, V}) || {K, V} <- [{a, 1}, {b, 2}]],
|
||||||
|
[ok = mnesia:dirty_write({d, K, V}) || {K, V} <- [{x, 10}, {y, 20}]],
|
||||||
|
|
||||||
|
BackupDir = backup_dir(Config, "restore_cf"),
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:backup(BackupDir)),
|
||||||
|
|
||||||
|
%% Mutate after backup — restore must discard these changes.
|
||||||
|
ok = mnesia:dirty_write({t, a, 999}),
|
||||||
|
ok = mnesia:dirty_write({t, c, 3}),
|
||||||
|
ok = mnesia:dirty_delete({d, x}),
|
||||||
|
ok = mnesia:dirty_write({d, z, 30}),
|
||||||
|
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:restore(BackupDir, #{start => true})),
|
||||||
|
?assertEqual(ok, mnesia:wait_for_tables([t, d], 10000)),
|
||||||
|
|
||||||
|
?assertEqual([{t, a, 1}, {t, b, 2}],
|
||||||
|
lists:sort(mnesia:dirty_match_object({t, '_', '_'}))),
|
||||||
|
?assertEqual([{d, x, 10}, {d, y, 20}],
|
||||||
|
lists:sort(mnesia:dirty_match_object({d, '_', '_'}))),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
restore_roundtrip_with_standalone(Config) ->
|
||||||
|
create_rdb_table(t, []),
|
||||||
|
create_rdb_table(ts, [{user_properties, [{rocksdb_standalone, true}]}]),
|
||||||
|
{atomic, ok} =
|
||||||
|
mnesia:create_table(d, [{disc_copies, [node()]},
|
||||||
|
{attributes, [k, v]}]),
|
||||||
|
ok = mnesia:wait_for_tables([t, ts, d], 5000),
|
||||||
|
|
||||||
|
ok = mnesia:dirty_write({t, 1, one}),
|
||||||
|
ok = mnesia:dirty_write({ts, 2, two}),
|
||||||
|
ok = mnesia:dirty_write({d, 3, three}),
|
||||||
|
#{type := standalone} = mrdb:get_ref(ts),
|
||||||
|
|
||||||
|
BackupDir = backup_dir(Config, "restore_sa"),
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:backup(BackupDir)),
|
||||||
|
|
||||||
|
ok = mnesia:dirty_write({t, 1, changed}),
|
||||||
|
ok = mnesia:dirty_write({ts, 2, changed}),
|
||||||
|
ok = mnesia:dirty_write({d, 3, changed}),
|
||||||
|
|
||||||
|
?assertEqual(ok, mnesia_rocksdb:restore(BackupDir, #{start => true})),
|
||||||
|
?assertEqual(ok, mnesia:wait_for_tables([t, ts, d], 10000)),
|
||||||
|
|
||||||
|
?assertEqual([{t, 1, one}], mnesia:dirty_read(t, 1)),
|
||||||
|
?assertEqual([{ts, 2, two}], mnesia:dirty_read(ts, 2)),
|
||||||
|
?assertEqual([{d, 3, three}], mnesia:dirty_read(d, 3)),
|
||||||
|
#{type := standalone} = mrdb:get_ref(ts),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
create_rdb_table(Name, Extra) ->
|
||||||
|
{atomic, ok} =
|
||||||
|
mnesia:create_table(Name, [{rdb, [node()]},
|
||||||
|
{attributes, [k, v]} | Extra]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
backup_dir(Config, Name) ->
|
||||||
|
filename:join(?config(priv_dir, Config), "backup_" ++ Name).
|
||||||
|
|
||||||
|
assert_layout(BackupDir) ->
|
||||||
|
?assert(filelib:is_regular(filename:join(BackupDir, "MANIFEST"))),
|
||||||
|
?assert(filelib:is_regular(filename:join(BackupDir, "mnesia.BUP"))),
|
||||||
|
?assert(filelib:is_dir(filename:join(BackupDir, "rocksdb"))).
|
||||||
|
|
||||||
|
read_manifest(BackupDir) ->
|
||||||
|
Path = filename:join(BackupDir, "MANIFEST"),
|
||||||
|
{ok, [Manifest]} = file:consult(Path),
|
||||||
|
Manifest.
|
||||||
|
|
||||||
|
assert_admin_backup_ok(BackupDir, Dbs) ->
|
||||||
|
[Admin | _] = [D || #{kind := admin} = D <- Dbs],
|
||||||
|
#{backup_relpath := Rel, backup_id := Id} = Admin,
|
||||||
|
Abs = filename:join(BackupDir, Rel),
|
||||||
|
{ok, BE} = rocksdb:open_backup_engine(Abs),
|
||||||
|
try
|
||||||
|
Infos = case rocksdb:get_backup_info(BE) of
|
||||||
|
{ok, L} -> L;
|
||||||
|
L when is_list(L) -> L
|
||||||
|
end,
|
||||||
|
?assertNotEqual([], Infos),
|
||||||
|
?assertEqual(ok, rocksdb:verify_backup(BE, Id))
|
||||||
|
after
|
||||||
|
rocksdb:close_backup_engine(BE)
|
||||||
|
end.
|
||||||
|
|
||||||
|
assert_mnesia_bup_has_table(BackupDir, Tab) ->
|
||||||
|
Bup = filename:join(BackupDir, "mnesia.BUP"),
|
||||||
|
%% Fun is applied per backup item (not per batch).
|
||||||
|
{ok, Found} =
|
||||||
|
mnesia:traverse_backup(
|
||||||
|
Bup, mnesia_backup, read_only, read_only,
|
||||||
|
fun(Rec, _Acc) when is_tuple(Rec), element(1, Rec) =:= Tab ->
|
||||||
|
{[Rec], true};
|
||||||
|
(Rec, Acc) ->
|
||||||
|
{[Rec], Acc}
|
||||||
|
end, false),
|
||||||
|
?assertEqual(true, Found).
|
||||||
|
|
||||||
|
assert_mnesia_bup_lacks_table_data(BackupDir, Tab) ->
|
||||||
|
Bup = filename:join(BackupDir, "mnesia.BUP"),
|
||||||
|
{ok, Found} =
|
||||||
|
mnesia:traverse_backup(
|
||||||
|
Bup, mnesia_backup, read_only, read_only,
|
||||||
|
fun({schema, _, _} = Rec, Acc) ->
|
||||||
|
{[Rec], Acc};
|
||||||
|
(Rec, _Acc) when is_tuple(Rec), element(1, Rec) =:= Tab ->
|
||||||
|
{[Rec], true};
|
||||||
|
(Rec, Acc) ->
|
||||||
|
{[Rec], Acc}
|
||||||
|
end, false),
|
||||||
|
%% Data records for Tab should not appear; schema may still mention Tab.
|
||||||
|
?assertEqual(false, Found).
|
||||||
Reference in New Issue
Block a user