-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 ]). -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 ]} ]. 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. 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}.