Files
mnesia_rocksdb/test/mnesia_rocksdb_migration_SUITE.erl
T

422 lines
15 KiB
Erlang

-module(mnesia_rocksdb_migration_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([
manual_migration/1
, migrate_with_encoding_change/1
, auto_migration/1
, online_encoding_migration/1
, online_encoding_migration_restart/1
, online_encoding_migration_interrupt/1
, change_table_type_sync/1
, change_table_type_async/1
, change_table_type_schema_only/1
, change_table_type_no_change/1
]).
-include_lib("common_test/include/ct.hrl").
-define(TABS_CREATED, tables_created).
suite() ->
[].
all() ->
[{group, all_tests}].
groups() ->
[
{all_tests, [sequence], [ manual_migration
, migrate_with_encoding_change
, online_encoding_migration
, online_encoding_migration_restart
, online_encoding_migration_interrupt
, change_table_type_sync
, change_table_type_async
, change_table_type_schema_only
, change_table_type_no_change ]}
].
init_per_suite(Config) ->
Config.
end_per_suite(_Config) ->
ok.
init_per_group(_, Config) ->
Config.
end_per_group(_, _Config) ->
ok.
init_per_testcase(_, Config) ->
mnesia:stop(),
ok = mnesia_rocksdb_tlib:start_mnesia(reset),
Config.
%% create_migrateable_db(Config).
end_per_testcase(_, _Config) ->
ok.
manual_migration(Config) ->
tr_ct:with_trace(fun manual_migration_/1, Config, tr_opts()).
manual_migration_(Config) ->
create_migrateable_db(Config),
Tabs = tables(),
ct:log("Analyze (before): ~p", [analyze_tabs(Tabs)]),
Res = mnesia_rocksdb_admin:migrate_standalone(rdb, Tabs),
ct:log("migrate_standalone(rdb, ~p) -> ~p", [Tabs, Res]),
AnalyzeRes = analyze_tabs(Tabs),
ct:log("AnalyzeRes = ~p", [AnalyzeRes]),
MigRes = mnesia_rocksdb_admin:migrate_standalone(rdb, Tabs),
ct:log("MigRes = ~p", [MigRes]),
AnalyzeRes2 = analyze_tabs(Tabs),
ct:log("AnalyzeRes2 = ~p", [AnalyzeRes2]),
ct:log("Admin State = ~p", [sys:get_state(mnesia_rocksdb_admin)]),
ok.
migrate_with_encoding_change(_Config) ->
ok = create_tab(t, [{user_properties, [{mrdb_encoding, {sext,{object,term}}},
{rocksdb_standalone, true}]},
{index,[val]}
]),
mrdb:insert(t, {t, <<"1">>, <<"a">>}),
mrdb:insert(t, {t, <<"2">>, <<"b">>}),
TRef = mrdb:get_ref(t),
{ok, V1} = mrdb:rdb_get(TRef, sext:encode(<<"1">>), []),
{ok, V2} = mrdb:rdb_get(TRef, sext:encode(<<"2">>), []),
{t,[],<<"a">>} = binary_to_term(V1),
{t,[],<<"b">>} = binary_to_term(V2),
Opts = #{encoding => {raw, raw}},
MigRes = mnesia_rocksdb_admin:migrate_standalone(rdb, [{t, Opts}]),
ct:log("MigRes (t) = ~p", [MigRes]),
%%
%% Ensure that metadata reflect the migrated table
%% (now a column family, and the rocksdb_standalone prop gone)
%%
TRef1 = mrdb:get_ref(t),
ct:log("TRef1(t) = ~p", [TRef1]),
#{type := column_family,
properties := #{user_properties := UPs}} = TRef1,
error = maps:find(rocksdb_standalone, UPs),
UPsR = lists:sort(maps:values(UPs)),
UPsM = lists:sort(mnesia:table_info(t, user_properties)),
{UPsR,UPsM} = {UPsM,UPsR},
ct:log("user properties (t): ~p", [UPsM]),
[{<<"2">>, <<"b">>},
{<<"1">>, <<"a">>}] = mrdb:rdb_fold(
t, fun(K,V,A) -> [{K,V}|A] end, [], <<>>),
ct:log("All data present in new column family", []),
ct:log("Contents of mnesia dir: ~p",
[ok(file:list_dir(mnesia:system_info(directory)))]),
ct:log("mnesia stopped", []),
mnesia:stop(),
mnesia:start(),
ct:log("mnesia started", []),
mnesia:info(),
ok = mnesia:wait_for_tables([t], 3000),
ct:log("tables loaded", []),
[{t,<<"1">>,<<"a">>},
{t,<<"2">>,<<"b">>}] = mrdb:select(
t, [{'_',[],['$_']}]),
[{<<"2">>,<<"b">>},
{<<"1">>,<<"a">>}] = mrdb:rdb_fold(
t, fun(K,V,A) -> [{K,V}|A] end, [], <<>>),
ok.
auto_migration(_Config) ->
ok.
%% Online encoding migration: term keys -> sext keys while dual-writing.
online_encoding_migration(_Config) ->
ok = create_tab(enc, [{attributes, [k, v]}]),
%% Default set encoding is {term, {value, term}} for 2-attr tables.
lists:foreach(
fun(I) ->
mrdb:insert(enc, {enc, I, I * 10})
end, lists:seq(1, 50)),
50 = length(mrdb:select(enc, [{'_', [], ['$_']}])),
Ref0 = mrdb:get_ref(enc),
ct:log("Before migration ref: ~p", [maps:with([encoding, type, semantics], Ref0)]),
{term, _} = maps:get(encoding, Ref0),
%% Concurrent writes during migration
ok = mnesia_rocksdb_admin:migrate_encoding(
rdb, enc, {sext, {value, term}}),
%% Dual-write should be active
#{migration := _} = mrdb:get_ref(enc),
mrdb:insert(enc, {enc, 100, 1000}),
mrdb:delete(enc, 5),
%% Wait for copy_done
ok = wait_copy_done(enc, 50),
ct:log("Status after copy: ~p", [mnesia_rocksdb_admin:migration_status(enc)]),
ok = mnesia_rocksdb_admin:finalize_migration(rdb, enc),
Ref1 = mrdb:get_ref(enc),
ct:log("After finalize ref: ~p", [maps:with([encoding, type], Ref1)]),
{sext, _} = maps:get(encoding, Ref1),
false = maps:is_key(migration, Ref1),
Objs = lists:sort(mrdb:select(enc, [{'_', [], ['$_']}])),
%% 50 - 1 deleted + 1 inserted = 50
50 = length(Objs),
false = lists:keymember(5, 2, Objs),
true = lists:keymember(100, 2, Objs),
{enc, 100, 1000} = lists:keyfind(100, 2, Objs),
%% sext-encoded key is readable via rdb_get
{ok, _} = mrdb:rdb_get(Ref1, sext:encode(100), []),
ok.
wait_copy_done(_Tab, 0) ->
{error, timeout};
wait_copy_done(Tab, N) ->
case mnesia_rocksdb_admin:migration_status(Tab) of
#{phase := copy_done} ->
ok;
_ ->
timer:sleep(50),
wait_copy_done(Tab, N - 1)
end.
%% Complete migration, restart mnesia, verify encoding + data survive.
online_encoding_migration_restart(_Config) ->
ok = create_tab(enc_r, [{attributes, [k, v]}]),
[mrdb:insert(enc_r, {enc_r, I, I * 3}) || I <- lists:seq(1, 30)],
ok = mnesia_rocksdb_admin:migrate_encoding(
rdb, enc_r, {sext, {value, term}}),
ok = wait_copy_done(enc_r, 100),
ok = mnesia_rocksdb_admin:finalize_migration(rdb, enc_r),
{sext, _} = maps:get(encoding, mrdb:get_ref(enc_r)),
Before = lists:sort(mrdb:select(enc_r, [{'_', [], ['$_']}])),
30 = length(Before),
%% Close and reopen DB (same VM: stop/start mnesia + wait for tables).
stopped = mnesia:stop(),
ok = mnesia:start(),
ok = mnesia:wait_for_tables([enc_r], 10000),
Ref = mrdb:get_ref(enc_r),
ct:log("After restart ref: ~p",
[maps:with([encoding, cf_gen, type], Ref)]),
{sext, _} = maps:get(encoding, Ref),
false = maps:is_key(migration, Ref),
After = lists:sort(mrdb:select(enc_r, [{'_', [], ['$_']}])),
Before = After,
{ok, _} = mrdb:rdb_get(Ref, sext:encode(1), []),
ok.
%% Stop mid-migration (after dual-write is up), reopen, finish copy + finalize.
online_encoding_migration_interrupt(_Config) ->
ok = create_tab(enc_i, [{attributes, [k, v]}]),
[mrdb:insert(enc_i, {enc_i, I, I}) || I <- lists:seq(1, 100)],
ok = mnesia_rocksdb_admin:migrate_encoding(
rdb, enc_i, {sext, {value, term}}),
%% Dual-write active; do not wait for copy_done — interrupt promptly.
#{migration := _} = mrdb:get_ref(enc_i),
mrdb:insert(enc_i, {enc_i, 200, 200}),
mrdb:delete(enc_i, 10),
stopped = mnesia:stop(),
ok = mnesia:start(),
ok = mnesia:wait_for_tables([enc_i], 10000),
%% Migration should be re-armed (dual-write) or already copy_done.
case mnesia_rocksdb_admin:migration_status(enc_i) of
#{phase := Phase} when Phase =:= copying; Phase =:= copy_done ->
ok;
Other ->
ct:fail({expected_migration_after_restart, Other})
end,
%% Writes during resumed dual-write
mrdb:insert(enc_i, {enc_i, 201, 201}),
ok = wait_copy_done(enc_i, 200),
ok = mnesia_rocksdb_admin:finalize_migration(rdb, enc_i),
{sext, _} = maps:get(encoding, mrdb:get_ref(enc_i)),
false = maps:is_key(migration, mrdb:get_ref(enc_i)),
Objs = lists:sort(mrdb:select(enc_i, [{'_', [], ['$_']}])),
false = lists:keymember(10, 2, Objs),
true = lists:keymember(200, 2, Objs),
true = lists:keymember(201, 2, Objs),
%% 100 - 1 delete + 2 inserts = 101
101 = length(Objs),
ok.
%% =====================================================================
%% change_table_type/3 and /4
%% =====================================================================
%% change_table_type/3 defaults to wait=true: copy + finalize in one call.
%% set (term keys) -> ordered_set implies default sext key encoding migration.
change_table_type_sync(_Config) ->
ok = create_tab(ctt_s, [{attributes, [k, v]}]),
set = mnesia:table_info(ctt_s, type),
[mrdb:insert(ctt_s, {ctt_s, I, I * 2}) || I <- lists:seq(1, 40)],
Ref0 = mrdb:get_ref(ctt_s),
{term, _} = maps:get(encoding, Ref0),
set = maps:get(type, maps:get(properties, Ref0)),
ok = mnesia_rocksdb_admin:change_table_type(
rdb, ctt_s, #{type => ordered_set, report => false}),
idle = mnesia_rocksdb_admin:migration_status(ctt_s),
Ref1 = mrdb:get_ref(ctt_s),
false = maps:is_key(migration, Ref1),
{sext, _} = maps:get(encoding, Ref1),
ordered_set = maps:get(type, maps:get(properties, Ref1)),
ordered_set = maps:get(semantics, Ref1),
ordered_set = mnesia:table_info(ctt_s, type),
assert_mrdb_encoding_up(ctt_s, {sext, {value, term}}),
Objs = lists:sort(mrdb:select(ctt_s, [{'_', [], ['$_']}])),
40 = length(Objs),
{ok, _} = mrdb:rdb_get(Ref1, sext:encode(1), []),
%% Writes after change use ordered_set / sext
ok = mrdb:insert(ctt_s, {ctt_s, 100, 200}),
{ctt_s, 100, 200} = lists:keyfind(100, 2, mrdb:select(ctt_s, [{'_', [], ['$_']}])),
{ok, _} = mrdb:rdb_get(mrdb:get_ref(ctt_s), sext:encode(100), []),
ok.
%% change_table_type/4 defaults to wait=false: start only; finalize manually.
change_table_type_async(_Config) ->
ok = create_tab(ctt_a, [{attributes, [k, v]}]),
[mrdb:insert(ctt_a, {ctt_a, I, I}) || I <- lists:seq(1, 25)],
Self = self(),
ok = mnesia_rocksdb_admin:change_table_type(
rdb, ctt_a, #{type => ordered_set, encoding => {sext, {value, term}}},
Self),
%% Dual-write should be on immediately; type not cut over yet.
#{migration := _} = mrdb:get_ref(ctt_a),
set = mnesia:table_info(ctt_a, type),
{term, _} = maps:get(encoding, mrdb:get_ref(ctt_a)),
mrdb:insert(ctt_a, {ctt_a, 50, 50}),
mrdb:delete(ctt_a, 3),
ok = wait_copy_done(ctt_a, 100),
ok = mnesia_rocksdb_admin:finalize_migration(rdb, ctt_a),
idle = mnesia_rocksdb_admin:migration_status(ctt_a),
Ref1 = mrdb:get_ref(ctt_a),
false = maps:is_key(migration, Ref1),
{sext, _} = maps:get(encoding, Ref1),
ordered_set = mnesia:table_info(ctt_a, type),
ordered_set = maps:get(type, maps:get(properties, Ref1)),
Objs = lists:sort(mrdb:select(ctt_a, [{'_', [], ['$_']}])),
false = lists:keymember(3, 2, Objs),
true = lists:keymember(50, 2, Objs),
%% 25 - 1 delete + 1 insert = 25
25 = length(Objs),
{ok, _} = mrdb:rdb_get(Ref1, sext:encode(50), []),
ok.
%% Type change only when encoding already matches the default for the new type
%% (set with sext -> ordered_set with same sext): schema/metadata, no CF copy.
change_table_type_schema_only(_Config) ->
ok = create_tab(ctt_so,
[{attributes, [k, v]},
{user_properties,
[{mrdb_encoding, {sext, {value, term}}}]}]),
set = mnesia:table_info(ctt_so, type),
Ref0 = mrdb:get_ref(ctt_so),
{sext, _} = maps:get(encoding, Ref0),
CfGen0 = maps:get(cf_gen, Ref0, 0),
[mrdb:insert(ctt_so, {ctt_so, I, I}) || I <- lists:seq(1, 10)],
ok = mnesia_rocksdb_admin:change_table_type(
rdb, ctt_so, #{type => ordered_set, report => false}),
idle = mnesia_rocksdb_admin:migration_status(ctt_so),
Ref1 = mrdb:get_ref(ctt_so),
false = maps:is_key(migration, Ref1),
{sext, _} = maps:get(encoding, Ref1),
CfGen0 = maps:get(cf_gen, Ref1, 0),
ordered_set = mnesia:table_info(ctt_so, type),
ordered_set = maps:get(type, maps:get(properties, Ref1)),
ordered_set = maps:get(semantics, Ref1),
10 = length(mrdb:select(ctt_so, [{'_', [], ['$_']}])),
{ok, _} = mrdb:rdb_get(Ref1, sext:encode(1), []),
ok.
%% Idempotent / no-op errors.
change_table_type_no_change(_Config) ->
ok = create_tab(ctt_nc, [{attributes, [k, v]}]),
set = mnesia:table_info(ctt_nc, type),
{error, no_change} =
mnesia_rocksdb_admin:change_table_type(
rdb, ctt_nc, #{type => set, report => false}),
%% Same encoding as live ref
{term, ValEnc} = maps:get(encoding, mrdb:get_ref(ctt_nc)),
{error, no_change} =
mnesia_rocksdb_admin:change_table_type(
rdb, ctt_nc, #{encoding => {term, ValEnc}, report => false}),
ok.
assert_mrdb_encoding_up(Tab, Enc) ->
UPs = mnesia:table_info(Tab, user_properties),
{mrdb_encoding, Enc} = lists:keyfind(mrdb_encoding, 1, UPs),
#{properties := #{user_properties := UPMap}} = mrdb:get_ref(Tab),
{mrdb_encoding, Enc} = maps:get(mrdb_encoding, UPMap),
ok.
ok({ok, Value}) -> Value.
tr_opts() ->
#{ patterns => [ {mnesia_rocksdb_admin, '_', []}
, {mnesia_rocksdb_lib, '_', []}
, {rocksdb, '_', x} | trace_exports(mrdb, x) ] }.
trace_exports(M, Pat) ->
Fs = M:module_info(exports),
[{M, F, A, Pat} || {F, A} <- Fs].
tables() ->
[a].
create_migrateable_db(Config) ->
Os = [{user_properties, [{rocksdb_standalone, true}]}],
TabNames = tables(),
Tabs = [{T, Os} || T <- TabNames],
create_tabs(Tabs, Config),
verify_tabs_are_standalone(TabNames),
fill_tabs(TabNames),
Config.
fill_tabs(Tabs) ->
%% Fill with more than 300, since that's the currently hard-coded chunk size
lists:foreach(fun(Tab) ->
[mrdb:insert(Tab, {Tab, X, a}) || X <- lists:seq(1,500)]
end, Tabs).
create_tabs(Tabs, Config) ->
Res = lists:map(fun create_tab/1, Tabs),
tr_ct:trace_checkpoint(?TABS_CREATED, Config),
Res.
create_tab({T, Opts}) ->
create_tab(T, Opts).
create_tab(T, Opts) ->
{atomic, ok} = mnesia:create_table(T, [{rdb, [node()]} | Opts]),
ok.
verify_tabs_are_standalone(Tabs) ->
case analyze_tabs(Tabs) of
{_, []} ->
ok;
{[], NotSA} ->
error({not_standalone, NotSA})
end.
analyze_tabs(Tabs) ->
Dir = mnesia:system_info(directory),
Files = filelib:wildcard(filename:join(Dir, "*-_tab.extrdb")),
ct:log("Files = ~p", [Files]),
TabNames = lists:map(
fun(F) ->
{match,[TStr]} =
re:run(F, "^.+/([^/]+)-_tab\\.extrdb$",
[{capture, [1], list}]),
list_to_existing_atom(TStr)
end, Files),
ct:log("TabNames = ~p", [TabNames]),
NotSA = Tabs -- TabNames,
{TabNames -- NotSA, NotSA}.