Migrate table encoding and type #11
+367
-145
@@ -12,20 +12,23 @@
|
||||
, related_resources/2 %% (Alias, Name) -> [RelatedTab]
|
||||
, prep_close/2 %% (Alias, Tab) -> ok
|
||||
, get_ref/1 %% (Name) -> Ref | abort()
|
||||
, get_ref/2 %% (Name, Default -> Ref | Default
|
||||
, request_ref/2 %% (Alias, Name) -> Ref
|
||||
, get_ref/2 %% (Name, Default) -> Ref | Default
|
||||
, request_ref/1 %% (Name) -> {ok, Ref} | {error, _}
|
||||
, request_ref/2 %% (Alias, Name) -> {ok, Ref} | {error, _}
|
||||
, close_table/2
|
||||
, clear_table/1
|
||||
]).
|
||||
|
||||
-export([ migrate_standalone/2
|
||||
, migrate_standalone/3
|
||||
%% Online encoding migration (set tables): dual-write + CF copy
|
||||
%% Online encoding / type migration: dual-write + CF copy
|
||||
, change_table_type/3
|
||||
, change_table_type/4
|
||||
, migrate_encoding/3
|
||||
, migrate_encoding/4
|
||||
, migration_status/1
|
||||
, finalize_migration/2
|
||||
, abort_migration/2
|
||||
]).
|
||||
|
||||
-export([ start_link/0
|
||||
@@ -74,8 +77,17 @@
|
||||
-type db_ref() :: rocksdb:db_handle().
|
||||
-type properties() :: [{atom(), any()}].
|
||||
|
||||
%% Options for encoding/type migration.
|
||||
%% - `type': mnesia table type (set | ordered_set)
|
||||
%% - `encoding': rocksdb key/value encoding
|
||||
%% - `wait': when true (default for change_table_type/3), wait for copy and finalize
|
||||
%% - `timeout': wait timeout (default 5 minutes)
|
||||
%% - `report': print progress while waiting (default true)
|
||||
-type encoding_opt() :: #{ type => set | ordered_set
|
||||
, encoding => any() }.
|
||||
, encoding => any()
|
||||
, wait => boolean()
|
||||
, timeout => timeout()
|
||||
, report => boolean() }.
|
||||
|
||||
-type cf() :: mrdb:db_ref().
|
||||
|
||||
@@ -91,8 +103,10 @@
|
||||
| {remove_aliases, [alias()]}
|
||||
| {migrate, [tabname() | {tabname(), map()}], rpt()}
|
||||
| {migrate_encoding, tabname(), any(), rpt()}
|
||||
| {change_table_type, tabname(), encoding_opt(), rpt()}
|
||||
| {migration_status, tabname()}
|
||||
| {finalize_migration, tabname()}
|
||||
| {abort_migration, tabname()}
|
||||
| {prep_close, table()}
|
||||
| {close_table, table()}
|
||||
| {clear_table, table() | cf() }.
|
||||
@@ -211,8 +225,32 @@ get_ref(Name) ->
|
||||
Other
|
||||
end.
|
||||
|
||||
%% Hot path: persistent_term. On miss (after erase_pt opens a sync window),
|
||||
%% fall back to request_ref/1 so the caller waits on the admin gen_server.
|
||||
%% request_ref only reads admin state — it does not put_pt; the writer that
|
||||
%% erased PT is responsible for put_pt when the update completes.
|
||||
%% Avoid re-entering when already inside the admin process.
|
||||
get_ref(Name, Default) ->
|
||||
get_pt(Name, Default).
|
||||
case get_pt(Name, error) of
|
||||
error ->
|
||||
case whereis(?MODULE) of
|
||||
Pid when Pid =:= self() ->
|
||||
Default;
|
||||
Pid when is_pid(Pid) ->
|
||||
case request_ref(Name) of
|
||||
{ok, Ref} -> Ref;
|
||||
_ -> Default
|
||||
end;
|
||||
undefined ->
|
||||
Default
|
||||
end;
|
||||
Ref ->
|
||||
Ref
|
||||
end.
|
||||
|
||||
%% Name-only: resolve table across aliases via admin (read-only; no put_pt).
|
||||
request_ref(Name) ->
|
||||
call([], {get_ref, Name}).
|
||||
|
||||
request_ref(Alias, Name) ->
|
||||
call(Alias, {get_ref, Name}).
|
||||
@@ -303,59 +341,126 @@ migrate_standalone(Alias, Tabs, Rpt0) ->
|
||||
end,
|
||||
call(Alias, {migrate, Tabs, Rpt}).
|
||||
|
||||
%% @doc Change mnesia table `type' and/or rocksdb encoding.
|
||||
%%
|
||||
%% `Opts' may include:
|
||||
%% <ul>
|
||||
%% <li>`type' - `set' | `ordered_set'</li>
|
||||
%% <li>`encoding' - target key/value encoding</li>
|
||||
%% <li>`wait' - `true' (default): wait for copy and finalize;
|
||||
%% `false': start dual-write + copier only; call
|
||||
%% {@link finalize_migration/2} later</li>
|
||||
%% <li>`timeout' - wait timeout (default 5 minutes)</li>
|
||||
%% <li>`report' - print progress while waiting (default `true')</li>
|
||||
%% </ul>
|
||||
%%
|
||||
%% If only `type' changes and the current encoding already matches the
|
||||
%% default for the new type, only the mnesia schema / admin metadata are
|
||||
%% updated. Otherwise an online CF encoding migration is started.
|
||||
-spec change_table_type(Alias, Tab, Opts) -> ok | {error, any()}
|
||||
when Alias :: alias()
|
||||
, Tab :: tabname()
|
||||
, Opts :: encoding_opt().
|
||||
change_table_type(Alias, Tab, Opts) ->
|
||||
A = erlang:alias(),
|
||||
case change_table_type(Alias, Tab, Opts, #{to => A}) of
|
||||
ok ->
|
||||
case collect_reports(#{ success => encoding_migrator_done
|
||||
, failure => encoding_migrator_failed
|
||||
, alias => A
|
||||
, report => true
|
||||
, timeout => timer:minutes(5) }) of
|
||||
ok ->
|
||||
finalize_migration(Alias, Tab)
|
||||
end;
|
||||
{error,_} = Error ->
|
||||
abort_migration(Alias, Tab),
|
||||
Error
|
||||
end.
|
||||
change_table_type(Alias, Tab, Opts) when is_map(Opts) ->
|
||||
Wait = maps:get(wait, Opts, true),
|
||||
change_table_type_(Alias, Tab, Opts, Wait, undefined).
|
||||
|
||||
%% @doc Like {@link change_table_type/3}, with an explicit progress report target.
|
||||
%% Default for `wait' is `false' (start only) when `Rpt' is given.
|
||||
-spec change_table_type(Alias, Tab, Opts, Rpt) -> ok | {error, any()}
|
||||
when Alias :: alias()
|
||||
, Tab :: tabname()
|
||||
, Opts :: encoding_opt()
|
||||
, Rpt :: rpt().
|
||||
change_table_type(Alias, Tab, Opts, Rpt) ->
|
||||
call(Alias, {change_table_type, Tab, Opts, Rpt}).
|
||||
, Rpt :: rpt() | pid() | atom().
|
||||
change_table_type(Alias, Tab, Opts, Rpt) when is_map(Opts) ->
|
||||
Wait = maps:get(wait, Opts, false),
|
||||
change_table_type_(Alias, Tab, Opts, Wait, Rpt).
|
||||
|
||||
%% @doc Start online encoding migration for a set table (dual-write + CF copy).
|
||||
-spec migrate_encoding(alias(), tabname(), Encoding :: encoding_opt()) ->
|
||||
ok | {error, term()}.
|
||||
change_table_type_(Alias, Tab, Opts, Wait, Rpt0) ->
|
||||
MigOpts = maps:with([type, encoding], Opts),
|
||||
{Rpt, WaitAlias} = prepare_mig_rpt(Rpt0, Wait, migrate_encoding),
|
||||
case call(Alias, {change_table_type, Tab, MigOpts, Rpt}) of
|
||||
ok when Wait ->
|
||||
wait_and_maybe_finalize(Alias, Tab, Opts, WaitAlias);
|
||||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
%% @doc Start online encoding migration (dual-write + CF copy). Does not wait
|
||||
%% for completion; call {@link finalize_migration/2} after copy_done.
|
||||
%% `Encoding' may be an encoding tuple or an options map.
|
||||
-spec migrate_encoding(alias(), tabname(), Encoding) ->
|
||||
ok | {error, term()}
|
||||
when Encoding :: encoding_opt() | any().
|
||||
migrate_encoding(Alias, Tab, Encoding) ->
|
||||
migrate_encoding(Alias, Tab, Encoding, undefined).
|
||||
|
||||
-spec migrate_encoding(alias(), tabname(), Encoding :: encoding_opt(), rpt()) ->
|
||||
ok | {error, term()}.
|
||||
-spec migrate_encoding(alias(), tabname(), Encoding, rpt() | pid() | atom()) ->
|
||||
ok | {error, term()}
|
||||
when Encoding :: encoding_opt() | any().
|
||||
migrate_encoding(Alias, Tab, Encoding, Rpt0) ->
|
||||
Rpt = case Rpt0 of
|
||||
undefined -> undefined;
|
||||
To when is_pid(To); is_atom(To) ->
|
||||
#{to => To, tag => migrate_encoding};
|
||||
#{} = M -> M
|
||||
end,
|
||||
call(Alias, {migrate_encoding, Tab, Encoding, Rpt}).
|
||||
Opts = normalize_mig_opts(Encoding),
|
||||
{Rpt, _} = prepare_mig_rpt(Rpt0, false, migrate_encoding),
|
||||
call(Alias, {migrate_encoding, Tab, Opts, Rpt}).
|
||||
|
||||
prepare_mig_rpt(undefined, false, _Tag) ->
|
||||
{undefined, undefined};
|
||||
prepare_mig_rpt(undefined, true, Tag) ->
|
||||
A = erlang:alias(),
|
||||
{#{to => A, tag => Tag}, A};
|
||||
prepare_mig_rpt(To, Wait, Tag) when is_pid(To); is_atom(To) ->
|
||||
prepare_mig_rpt(#{to => To, tag => Tag}, Wait, Tag);
|
||||
prepare_mig_rpt(#{to := To} = R, _Wait, _Tag) ->
|
||||
{R, To};
|
||||
prepare_mig_rpt(#{} = R, true, Tag) ->
|
||||
A = erlang:alias(),
|
||||
{R#{to => A, tag => Tag}, A};
|
||||
prepare_mig_rpt(#{} = R, false, Tag) ->
|
||||
{R#{tag => maps:get(tag, R, Tag)}, undefined}.
|
||||
|
||||
normalize_mig_opts(Opts) when is_map(Opts) ->
|
||||
maps:with([type, encoding], Opts);
|
||||
normalize_mig_opts(Encoding) ->
|
||||
#{encoding => Encoding}.
|
||||
|
||||
wait_and_maybe_finalize(Alias, Tab, Opts, WaitAlias) ->
|
||||
case migration_status(Tab) of
|
||||
idle ->
|
||||
%% Schema-only type change (no CF copy) already completed.
|
||||
maybe_unalias(WaitAlias),
|
||||
ok;
|
||||
#{} ->
|
||||
Timeout = maps:get(timeout, Opts, timer:minutes(5)),
|
||||
Report = maps:get(report, Opts, true),
|
||||
CollectOpts0 = #{ success => encoding_migrator_done
|
||||
, failure => encoding_migrator_failed
|
||||
, report => Report
|
||||
, timeout => Timeout },
|
||||
CollectOpts = case WaitAlias of
|
||||
undefined -> CollectOpts0;
|
||||
A -> CollectOpts0#{alias => A}
|
||||
end,
|
||||
case collect_reports(CollectOpts) of
|
||||
ok ->
|
||||
finalize_migration(Alias, Tab);
|
||||
error ->
|
||||
{error, migration_failed_or_timeout}
|
||||
end
|
||||
end.
|
||||
|
||||
maybe_unalias(undefined) -> ok;
|
||||
maybe_unalias(A) -> erlang:unalias(A).
|
||||
|
||||
%% Status lives in admin CF (`encoding_migration` info), not in persistent_term.
|
||||
%% Dual-write is indicated by `migration` on the live ref; phase/progress is
|
||||
%% durable admin metadata updated by the migrator without touching PT.
|
||||
-spec migration_status(tabname()) -> idle | map().
|
||||
migration_status(Tab) when is_atom(Tab) ->
|
||||
case get_ref(Tab, error) of
|
||||
#{migration := _} = Ref ->
|
||||
case maps:get(migration_meta, Ref, undefined) of
|
||||
#{migration := _, alias := Alias} ->
|
||||
case read_info(Alias, Tab, encoding_migration, undefined) of
|
||||
#{} = Meta -> Meta;
|
||||
undefined -> #{phase => dual_write}
|
||||
undefined -> #{phase => copying}
|
||||
end;
|
||||
_ ->
|
||||
idle
|
||||
@@ -503,6 +608,14 @@ handle_call({[], {add_aliases, Aliases}}, _From, St) ->
|
||||
handle_call({[], {remove_aliases, Aliases}}, _From, St) ->
|
||||
St1 = do_remove_aliases(Aliases, St),
|
||||
{reply, ok, St1};
|
||||
handle_call({[], {get_ref, Name}}, _From, St) ->
|
||||
%% request_ref/1: read-only lookup. Writers own erase_pt/put_pt.
|
||||
case find_cf(Name, St) of
|
||||
#{status := open} = Ref ->
|
||||
{reply, {ok, Ref}, St};
|
||||
_ ->
|
||||
{reply, {error, not_found}, St}
|
||||
end;
|
||||
handle_call({Alias, Req}, _From, St) ->
|
||||
handle_call_for_alias(Alias, Req, St);
|
||||
handle_call(_Req, _From, St) ->
|
||||
@@ -645,6 +758,8 @@ handle_req(Alias, {clear_table, Name}, Backend, #st{} = St) ->
|
||||
{reply, {error, not_found}, St}
|
||||
end;
|
||||
handle_req(Alias, {get_ref, Name}, Backend, #st{} = St) ->
|
||||
%% Read-only: do not put_pt here. The metadata writer that called
|
||||
%% erase_pt is responsible for put_pt when the update completes.
|
||||
case find_cf(Alias, Name, Backend, St) of
|
||||
{ok, #{status := open} = Ref} ->
|
||||
{reply, {ok, Ref}, St};
|
||||
@@ -658,9 +773,10 @@ handle_req(_Alias, {related_resources, Tab}, Backend, St) ->
|
||||
{reply, Res, St};
|
||||
handle_req(Alias, {write_table_property, Tab, Prop}, Backend, St) ->
|
||||
case find_cf(Alias, Tab, Backend, St) of
|
||||
{ok, #{status := opens} = Cf0} ->
|
||||
{ok, #{status := open} = Cf0} ->
|
||||
case mnesia_schema:schema_transaction(
|
||||
fun() ->
|
||||
%% Open sync window first (schema + admin meta inside).
|
||||
erase_pt(Tab),
|
||||
Cf = update_user_properties(Prop, Cf0),
|
||||
St1 = update_cf(Alias, Tab, Cf, St),
|
||||
@@ -1213,24 +1329,42 @@ try_refresh_cf(#{alias := Alias, name := Name, properties := Ps} = Cf, Props, St
|
||||
try_refresh_cf(_, _, _) ->
|
||||
false.
|
||||
|
||||
%% Update admin cf metadata. Call erase_pt before this when opening a larger
|
||||
%% sync window (e.g. around a schema transaction). Erase here too so standalone
|
||||
%% use still forces concurrent get_ref callers through request_ref.
|
||||
update_table_properties(Alias, Tab, Meta, St) ->
|
||||
case find_cf_from_state(Alias, Tab, St) of
|
||||
{ok, Cf} ->
|
||||
erase_pt(Tab),
|
||||
Cf1 = update_cf_props(Meta, Cf),
|
||||
St1 = update_cf(Alias, Tab, Cf1, St),
|
||||
put_pt(Tab, Cf1),
|
||||
update_cf(Alias, Tab, Cf1, St);
|
||||
St1;
|
||||
{error, _} ->
|
||||
error({unknown_cf, {Alias, Tab}})
|
||||
end.
|
||||
|
||||
update_cf_props(Meta, #{properties := Props, name := Name} = Cf) ->
|
||||
Props1 = lists:foldl(fun update_cf_prop_/2, Props, Meta),
|
||||
Cf1 = Cf#{properties := Props1},
|
||||
%% Keep top-level semantics aligned with mnesia type when type changes.
|
||||
case {is_atom(Name), lists:keyfind(type, 1, Meta)} of
|
||||
{true, {type, Type}} ->
|
||||
Cf1#{semantics => Type};
|
||||
_ ->
|
||||
Cf1
|
||||
end;
|
||||
update_cf_props(Meta, #{properties := Props} = Cf) ->
|
||||
Props1 = lists:foldl(fun update_cf_prop_/2, Props, Meta),
|
||||
Cf#{properties := Props1}.
|
||||
|
||||
update_cf_prop_({user_properties, UPs}, Ps) ->
|
||||
%% Store full property tuples (#{Key => Prop}) like props_to_map/2 and
|
||||
%% update_user_properties/2 — not bare values from a proplist merge.
|
||||
UPs0 = maps:get(user_properties, Ps, #{}),
|
||||
UPs1 = maps:merge(UPs0, maps:from_list(UPs)),
|
||||
UPs1 = maps:merge(
|
||||
UPs0,
|
||||
maps:from_list([{element(1, P), P} || P <- UPs])),
|
||||
Ps#{user_properties => UPs1};
|
||||
update_cf_prop_({K, V}, Ps) ->
|
||||
Ps#{K => V}.
|
||||
@@ -1652,28 +1786,57 @@ cf_name_to_data_gen(Cf) ->
|
||||
%% Online encoding migration (set tables)
|
||||
%% =====================================================================
|
||||
|
||||
start_change_table_type(Alias, Tab, Opts, Rpt, Backend, St) ->
|
||||
Ref = get_ref(Tab),
|
||||
case Opts of
|
||||
#{type := T} when not is_map_key(encoding, Opts) ->
|
||||
if T == set; T == ordered_set ->
|
||||
case maps:get(type, maps:get(properties, Ref)) of
|
||||
T ->
|
||||
{error, same_type};
|
||||
_ ->
|
||||
Meta = [{type, T}],
|
||||
update_mnesia_schema(
|
||||
Tab, Meta,
|
||||
fun() -> update_table_properties(
|
||||
Alias, Tab, Meta, St)
|
||||
end)
|
||||
end;
|
||||
start_change_table_type(Alias, Tab, Opts0, Rpt, Backend, St) ->
|
||||
Opts = normalize_mig_opts(Opts0),
|
||||
case find_cf(Alias, Tab, Backend, St) of
|
||||
{ok, #{status := open} = Ref} ->
|
||||
start_change_table_type_(Alias, Tab, Opts, Rpt, Ref, Backend, St);
|
||||
{ok, _} ->
|
||||
{error, not_open};
|
||||
error ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
start_change_table_type_(Alias, Tab, Opts, Rpt, Ref, Backend, St) ->
|
||||
Props = maps:get(properties, Ref),
|
||||
CurType = maps:get(type, Props),
|
||||
CurEnc = maps:get(encoding, Ref),
|
||||
As = maps:get(attributes, Props),
|
||||
NewType = maps:get(type, Opts, CurType),
|
||||
if NewType =/= set, NewType =/= ordered_set ->
|
||||
{error, invalid_type};
|
||||
true ->
|
||||
{error, invalid_type}
|
||||
DefaultEnc = mnesia_rocksdb_lib:default_encoding(Tab, NewType, As),
|
||||
%% Type-only: migrate encoding when current differs from default
|
||||
%% for the new type; otherwise schema/metadata only.
|
||||
NewEnc0 = maps:get(encoding, Opts,
|
||||
case NewType of
|
||||
CurType -> CurEnc;
|
||||
_ when CurEnc =:= DefaultEnc -> CurEnc;
|
||||
_ -> DefaultEnc
|
||||
end),
|
||||
case mnesia_rocksdb_lib:check_encoding(NewEnc0, As) of
|
||||
{ok, NewEnc} when NewEnc =/= CurEnc ->
|
||||
MigOpts = Opts#{type => NewType, encoding => NewEnc},
|
||||
start_encoding_migration(
|
||||
Alias, Tab, MigOpts, Rpt, Backend, St);
|
||||
{ok, _NewEnc} when NewType =/= CurType ->
|
||||
Meta = [{type, NewType}],
|
||||
case update_mnesia_schema(
|
||||
Tab, Meta,
|
||||
fun() ->
|
||||
{ok, update_table_properties(
|
||||
Alias, Tab, Meta, St)}
|
||||
end) of
|
||||
{ok, _} = Ok -> Ok;
|
||||
{error, _} = Err -> Err;
|
||||
Other -> {error, Other}
|
||||
end;
|
||||
_ ->
|
||||
%% Start a migration; metadata changed when finalized
|
||||
start_encoding_migration(Alias, Tab, Opts, Rpt, Backend, St)
|
||||
{ok, _} ->
|
||||
{error, no_change};
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
end
|
||||
end.
|
||||
|
||||
start_encoding_migration(Alias, Tab, Encoding0, Rpt, Backend, St)
|
||||
@@ -1699,7 +1862,8 @@ start_encoding_migration(Alias, Tab, Encoding0, Rpt, Backend, St)
|
||||
start_encoding_migration(_, _, _, _, _, _) ->
|
||||
{error, badarg}.
|
||||
|
||||
start_encoding_migration_(Alias, Tab, Opts, Rpt, OldRef, Backend, St) ->
|
||||
start_encoding_migration_(Alias, Tab, Opts0, Rpt, OldRef, Backend, St) ->
|
||||
Opts = normalize_mig_opts(Opts0),
|
||||
Props = maps:get(properties, OldRef),
|
||||
As = maps:get(attributes, Props),
|
||||
Enc0 = maps:get(encoding, Opts, undefined),
|
||||
@@ -1733,6 +1897,9 @@ create_and_start_encoding_mig(Alias, Tab, NewEnc, Schema, Rpt, OldRef,
|
||||
CfName = data_cf_name(Tab, NewGen),
|
||||
case create_column_family(DbRef, CfName, cfopts(), OldRef) of
|
||||
{ok, CfH} ->
|
||||
%% Open sync window before dual-write metadata is published.
|
||||
%% Writers own erase_pt/put_pt; request_ref does not reinstall PT.
|
||||
erase_pt(Tab),
|
||||
%% Target ref: same DB, new versioned CF, new encoding; no migration field.
|
||||
NewRef0 = maps:without(
|
||||
[migration, migration_meta, migration_epoch],
|
||||
@@ -1747,9 +1914,6 @@ create_and_start_encoding_mig(Alias, Tab, NewEnc, Schema, Rpt, OldRef,
|
||||
%% Persist encoding in user_properties *before* check_version_and_encoding
|
||||
%% so it is not replaced by the table default.
|
||||
NewRef1 = update_cf_props(Schema, check_version_and_encoding(NewRef0b)),
|
||||
%% NewRef1 = update_user_properties(
|
||||
%% {mrdb_encoding, NewEnc},
|
||||
%% check_version_and_encoding(NewRef0b)),
|
||||
NewRef = NewRef1#{encoding => NewEnc, cf_gen => NewGen, cf_name => CfName},
|
||||
Meta = #{ phase => copying
|
||||
, target => #{encoding => NewEnc, cf_gen => NewGen}
|
||||
@@ -1758,21 +1922,19 @@ create_and_start_encoding_mig(Alias, Tab, NewEnc, Schema, Rpt, OldRef,
|
||||
, cursor => '$first'
|
||||
, epoch => 1
|
||||
, started_at => erlang:system_time(millisecond) },
|
||||
io:fwrite("Meta = ~p~n", [Meta]),
|
||||
%% Live ref: dual-write to NewRef; reads still use Old CF handle.
|
||||
%% Durable migration state (phase/progress/schema) lives in admin CF.
|
||||
%% Live PT only carries dual-write target — migrator never updates PT.
|
||||
write_info(Alias, Tab, encoding_migration, Meta),
|
||||
%% Live ref: dual-write to NewRef; reads still use old CF handle.
|
||||
%% Keep live encoding as old; schema mrdb_encoding updated on finalize.
|
||||
LiveRef = OldRef#{ migration => NewRef
|
||||
, migration_meta => Meta
|
||||
, migration_epoch => 1
|
||||
, cf_gen => OldGen },
|
||||
write_info(Alias, Tab, encoding_migration, Meta),
|
||||
%% Keep live encoding as old for correct reads; only target has NewEnc.
|
||||
%% Schema mrdb_encoding is updated on finalize.
|
||||
LiveRef2 = LiveRef#{migration := NewRef},
|
||||
St1 = update_cf(Alias, Tab, LiveRef2, St),
|
||||
put_pt(Tab, LiveRef2),
|
||||
St1 = update_cf(Alias, Tab, LiveRef, St),
|
||||
put_pt(Tab, LiveRef),
|
||||
{Pid, _MRef} = spawn_monitor(
|
||||
fun() ->
|
||||
encoding_migrator(Alias, Tab, LiveRef2,
|
||||
encoding_migrator(Alias, Tab, LiveRef,
|
||||
NewRef, Meta, Rpt)
|
||||
end),
|
||||
rpt(Rpt, "Started encoding migration ~p gen ~p -> ~p (~s)~n",
|
||||
@@ -1847,15 +2009,17 @@ resume_encoding_migration_(Alias, Name, LiveRef, Meta, Enc, NewGen, SourceGen,
|
||||
NewRef1 = update_user_properties({mrdb_encoding, Enc}, NewRef),
|
||||
NewRef2 = NewRef1#{encoding => Enc},
|
||||
Meta1 = Meta#{phase => Phase},
|
||||
%% PT: dual-write only. Phase/progress stay in admin CF (Meta1 already there).
|
||||
Live2 = Live1#{ migration => NewRef2
|
||||
, migration_meta => Meta1
|
||||
, migration_epoch => maps:get(epoch, Meta, 1) },
|
||||
erase_pt(Name),
|
||||
St0 = update_cf(Alias, Name, Live2, St),
|
||||
put_pt(Name, Live2),
|
||||
St1 = case Phase of
|
||||
copying ->
|
||||
case maps:get(Name, St#st.migrators, undefined) of
|
||||
case maps:get(Name, St0#st.migrators, undefined) of
|
||||
Pid when is_pid(Pid) ->
|
||||
St;
|
||||
St0;
|
||||
_ ->
|
||||
{Pid, _} = spawn_monitor(
|
||||
fun() ->
|
||||
@@ -1863,77 +2027,110 @@ resume_encoding_migration_(Alias, Name, LiveRef, Meta, Enc, NewGen, SourceGen,
|
||||
Alias, Name, Live2, NewRef2, Meta1,
|
||||
undefined)
|
||||
end),
|
||||
St#st{migrators = maps:put(Name, Pid, St#st.migrators)}
|
||||
St0#st{migrators = maps:put(Name, Pid, St0#st.migrators)}
|
||||
end;
|
||||
copy_done ->
|
||||
St
|
||||
St0
|
||||
end,
|
||||
{Live2, St1}.
|
||||
|
||||
%% Background copier: walk old CF, install into new if missing (no clobber).
|
||||
%% Background copier: walk old CF via mrdb:with_iterator, install into new
|
||||
%% if missing (no clobber). Does not touch persistent_term.
|
||||
%%
|
||||
%% Durable progress is the last logical key visited (`cursor` in admin CF
|
||||
%% encoding_migration info). Select continuations are not used — they are not
|
||||
%% fit for persistent storage; iterator + last key is.
|
||||
encoding_migrator(Alias, Tab, OldRef, NewRef, Meta0, Rpt) ->
|
||||
try
|
||||
Chunk = 500,
|
||||
N = encoding_copy_loop(OldRef, NewRef, '$first', 0, Chunk, Rpt),
|
||||
OldOnly = maps:without(
|
||||
[migration, migration_meta, migration_epoch], OldRef),
|
||||
Cursor0 = maps:get(cursor, Meta0, '$first'),
|
||||
N0 = maps:get(copied, Meta0, 0),
|
||||
N = mrdb:with_iterator(
|
||||
OldOnly,
|
||||
fun(I) ->
|
||||
encoding_iter_loop(
|
||||
I, Alias, Tab, OldOnly, NewRef, Meta0,
|
||||
Cursor0, N0, Chunk, Rpt)
|
||||
end),
|
||||
Meta = Meta0#{ phase => copy_done
|
||||
, cursor => '$end'
|
||||
, epoch => 1
|
||||
, copied => N
|
||||
, finished_at => erlang:system_time(millisecond) },
|
||||
%% Update live PT meta to copy_done (keep dual-write).
|
||||
case get_ref(Tab, error) of
|
||||
#{migration := _} = Live ->
|
||||
put_pt(Tab, Live#{migration_meta => Meta});
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
write_info(Alias, Tab, encoding_migration, Meta),
|
||||
rpt(Rpt, encoding_migrator_done, "Encoding migration copy done for ~p (~p objs)~n", [Tab, N]),
|
||||
rpt(Rpt, encoding_migrator_done,
|
||||
"Encoding migration copy done for ~p (~p objs)~n", [Tab, N]),
|
||||
ok
|
||||
catch
|
||||
C:R:ST ->
|
||||
rpt(Rpt, encoding_migrator_failed, "encoding_migrator ~p failed: ~p:~p / ~p",
|
||||
rpt(Rpt, encoding_migrator_failed,
|
||||
"encoding_migrator ~p failed: ~p:~p / ~p",
|
||||
[Tab, C, R, ST]),
|
||||
error({C, R})
|
||||
end.
|
||||
|
||||
encoding_copy_loop(OldRef, NewRef, Cursor, N, Chunk, Rpt) ->
|
||||
{Batch, Cont} = encoding_select_chunk(OldRef, Cursor, Chunk),
|
||||
N1 = lists:foldl(
|
||||
fun(Obj, Acc) ->
|
||||
encoding_maybe_install(OldRef, NewRef, Obj),
|
||||
Acc + 1
|
||||
end, N, Batch),
|
||||
case Cont of
|
||||
'$end_of_table' ->
|
||||
N1;
|
||||
NextCursor ->
|
||||
maybe_progress(Rpt, N1),
|
||||
encoding_copy_loop(OldRef, NewRef, NextCursor, N1, Chunk, Rpt)
|
||||
encoding_iter_loop(I, Alias, Tab, OldRef, NewRef, Meta0, Cursor, N, Chunk, Rpt) ->
|
||||
case encoding_iter_seek(I, OldRef, Cursor) of
|
||||
{ok, Obj} ->
|
||||
encoding_iter_step(
|
||||
I, Alias, Tab, OldRef, NewRef, Meta0, Obj, N, 0, Chunk, Rpt);
|
||||
done ->
|
||||
N
|
||||
end.
|
||||
|
||||
%% Chunked select on the old CF. Continuations use mrdb:select/1 when available.
|
||||
encoding_select_chunk(OldRef, '$first', Limit) ->
|
||||
case mrdb:select(OldRef, [{'_', [], ['$_']}], Limit) of
|
||||
{L, Cont} when is_list(L) ->
|
||||
{L, {cont, Cont}};
|
||||
L when is_list(L) ->
|
||||
{L, '$end_of_table'};
|
||||
'$end_of_table' ->
|
||||
{[], '$end_of_table'}
|
||||
%% Seek to first object, or to the first object after a durable logical cursor.
|
||||
encoding_iter_seek(I, _Ref, '$first') ->
|
||||
case mrdb:iterator_move(I, first) of
|
||||
{ok, _} = Ok -> Ok;
|
||||
{error, _} -> done
|
||||
end;
|
||||
encoding_select_chunk(_OldRef, {cont, Cont}, _Limit) ->
|
||||
case mrdb:select(Cont) of
|
||||
{L, Cont1} when is_list(L) ->
|
||||
{L, {cont, Cont1}};
|
||||
L when is_list(L) ->
|
||||
{L, '$end_of_table'};
|
||||
'$end_of_table' ->
|
||||
{[], '$end_of_table'}
|
||||
encoding_iter_seek(I, Ref, LastKey) ->
|
||||
Enc = mnesia_rocksdb_lib:encode_key(LastKey, Ref),
|
||||
case mrdb:iterator_move(I, Enc) of
|
||||
{ok, Obj} ->
|
||||
KP = mnesia_rocksdb_lib:keypos(maps:get(name, Ref)),
|
||||
case element(KP, Obj) of
|
||||
LastKey ->
|
||||
case mrdb:iterator_move(I, next) of
|
||||
{ok, _} = Ok -> Ok;
|
||||
{error, _} -> done
|
||||
end;
|
||||
encoding_select_chunk(OldRef, _Other, Limit) ->
|
||||
%% Fallback: restart from first (safe, may re-process; install is idempotent).
|
||||
encoding_select_chunk(OldRef, '$first', Limit).
|
||||
_ ->
|
||||
%% First key >= LastKey in rocksdb order that is not LastKey
|
||||
{ok, Obj}
|
||||
end;
|
||||
{error, _} ->
|
||||
done
|
||||
end.
|
||||
|
||||
encoding_iter_step(I, Alias, Tab, OldRef, NewRef, Meta0, Obj, N, Since, Chunk, Rpt) ->
|
||||
encoding_maybe_install(OldRef, NewRef, Obj),
|
||||
N1 = N + 1,
|
||||
KP = mnesia_rocksdb_lib:keypos(maps:get(name, OldRef)),
|
||||
Key = element(KP, Obj),
|
||||
Since1 = Since + 1,
|
||||
case Since1 >= Chunk of
|
||||
true ->
|
||||
%% Durable logical cursor (last key successfully considered).
|
||||
write_info(Alias, Tab, encoding_migration,
|
||||
Meta0#{cursor => Key, copied => N1}),
|
||||
maybe_progress(Rpt, N1),
|
||||
encoding_iter_next(
|
||||
I, Alias, Tab, OldRef, NewRef, Meta0, N1, 0, Chunk, Rpt);
|
||||
false ->
|
||||
encoding_iter_next(
|
||||
I, Alias, Tab, OldRef, NewRef, Meta0, N1, Since1, Chunk, Rpt)
|
||||
end.
|
||||
|
||||
encoding_iter_next(I, Alias, Tab, OldRef, NewRef, Meta0, N, Since, Chunk, Rpt) ->
|
||||
case mrdb:iterator_move(I, next) of
|
||||
{ok, Obj} ->
|
||||
encoding_iter_step(
|
||||
I, Alias, Tab, OldRef, NewRef, Meta0, Obj, N, Since, Chunk, Rpt);
|
||||
{error, _} ->
|
||||
N
|
||||
end.
|
||||
|
||||
encoding_maybe_install(OldRef, NewRef, Obj) ->
|
||||
Name = maps:get(name, OldRef),
|
||||
@@ -1954,18 +2151,20 @@ encoding_maybe_install(OldRef, NewRef, Obj) ->
|
||||
end
|
||||
end.
|
||||
|
||||
do_finalize_encoding_migration(Alias, Tab, _Backend, St) ->
|
||||
%% Prefer PT (migrator updates phase there) over gen_server state.
|
||||
case get_ref(Tab, error) of
|
||||
#{migration := MigRef} = LiveRef ->
|
||||
Meta = maps:get(migration_meta, LiveRef, #{}),
|
||||
do_finalize_encoding_migration(Alias, Tab, Backend, St) ->
|
||||
LiveRef = live_ref_for_mig(Alias, Tab, Backend, St),
|
||||
case LiveRef of
|
||||
#{migration := MigRef} = LR ->
|
||||
%% Phase/schema from durable admin CF (migrator never updates PT).
|
||||
Meta = read_info(Alias, Tab, encoding_migration, #{}),
|
||||
case maps:get(phase, Meta, undefined) of
|
||||
copy_done ->
|
||||
update_mnesia_schema(
|
||||
Tab,
|
||||
maps:get(schema, Meta),
|
||||
maps:get(schema, Meta, []),
|
||||
fun() ->
|
||||
finalize_encoding_migration_(Alias, Tab, LiveRef, MigRef, St)
|
||||
finalize_encoding_migration_(
|
||||
Alias, Tab, LR, MigRef, Meta, St)
|
||||
end);
|
||||
Phase ->
|
||||
{error, {not_ready, Phase}}
|
||||
@@ -1976,25 +2175,42 @@ do_finalize_encoding_migration(Alias, Tab, _Backend, St) ->
|
||||
{error, not_migrating}
|
||||
end.
|
||||
|
||||
do_abort_migration(Alias, Tab, _Backend, St) ->
|
||||
case get_ref(Tab, error) of
|
||||
#{migration := _} = LiveRef ->
|
||||
abort_encoding_migration_(Alias, Tab, LiveRef, St);
|
||||
do_abort_migration(Alias, Tab, Backend, St) ->
|
||||
case live_ref_for_mig(Alias, Tab, Backend, St) of
|
||||
#{migration := _} = LR ->
|
||||
abort_encoding_migration_(Alias, Tab, LR, St);
|
||||
#{} ->
|
||||
{{error, not_migrating}, St};
|
||||
error ->
|
||||
{{error, not_found}, St}
|
||||
end.
|
||||
|
||||
live_ref_for_mig(Alias, Tab, Backend, St) ->
|
||||
case get_pt(Tab, error) of
|
||||
error ->
|
||||
case find_cf(Alias, Tab, Backend, St) of
|
||||
{ok, R} -> R;
|
||||
error -> error
|
||||
end;
|
||||
R ->
|
||||
R
|
||||
end.
|
||||
|
||||
%% Run F inside a schema transaction that updates table cstruct fields in Meta.
|
||||
%% erase_pt is done at the start of the transaction so the schema change and
|
||||
%% admin-metadata side effects in F share one get_ref/request_ref sync window.
|
||||
%% F is responsible for put_pt when done (or leave PT empty only on abort paths
|
||||
%% that will reinstall).
|
||||
update_mnesia_schema(Tab, Meta, F) ->
|
||||
case mnesia_schema:schema_transaction(
|
||||
fun() ->
|
||||
erase_pt(Tab),
|
||||
update_mnesia_schema_(Tab, Meta, F)
|
||||
end) of
|
||||
{atomic, Res} ->
|
||||
Res;
|
||||
{aborted, Error} ->
|
||||
Error
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
update_mnesia_schema_(Tab, Meta, F) ->
|
||||
@@ -2030,22 +2246,25 @@ ensure_writable(Tab) ->
|
||||
|
||||
%% Cutover: make the versioned target CF the live one and drop the old CF.
|
||||
%% No second copy — the migration target *is* the new generation.
|
||||
finalize_encoding_migration_(Alias, Tab, LiveRef, MigRef, St) ->
|
||||
%% Called inside update_mnesia_schema/3, which already erase_pt'd Tab.
|
||||
finalize_encoding_migration_(Alias, Tab, LiveRef, MigRef, Meta, St) ->
|
||||
#{db_ref := DbRef, cf_handle := OldCfH} = LiveRef,
|
||||
NewEnc = maps:get(encoding, MigRef),
|
||||
NewGen = maps:get(cf_gen, MigRef, maps:get(cf_gen, LiveRef, 0) + 1),
|
||||
Schema = maps:get(schema, Meta, []),
|
||||
LiveNew = clear_cf_migration(
|
||||
MigRef#{ name => Tab
|
||||
, status => open
|
||||
, encoding => NewEnc
|
||||
, cf_gen => NewGen }),
|
||||
LiveNew1 = update_user_properties({mrdb_encoding, NewEnc}, LiveNew),
|
||||
put_pt(Tab, LiveNew1),
|
||||
St1 = update_cf(Alias, Tab, LiveNew1, St),
|
||||
%% Durable live generation + schema encoding for restart.
|
||||
LiveNew1 = update_cf_props(Schema, LiveNew),
|
||||
LiveNew2 = LiveNew1#{encoding => NewEnc, cf_gen => NewGen},
|
||||
St1 = update_cf(Alias, Tab, LiveNew2, St),
|
||||
%% Durable live generation + clear migration marker for restart.
|
||||
write_info(Alias, Tab, cf_gen, NewGen),
|
||||
delete_info(Alias, Tab, encoding_migration),
|
||||
_ = maybe_write_user_props(LiveNew1),
|
||||
_ = maybe_write_user_props(LiveNew2),
|
||||
put_pt(Tab, LiveNew2),
|
||||
%% Drop previous generation CF (by handle; name may be {d,Tab} or {d,Tab,G}).
|
||||
ok = rocksdb:drop_column_family(DbRef, OldCfH),
|
||||
try rocksdb:destroy_column_family(DbRef, OldCfH) catch error:_ -> ok end,
|
||||
@@ -2061,16 +2280,19 @@ abort_encoding_migration_(Alias, Tab, LiveRef, St) ->
|
||||
erase_pt(Name),
|
||||
#{db_ref := DbRef, cf_handle := CfH} = MigRef,
|
||||
try rocksdb:drop_column_family(DbRef, CfH) catch error:_ -> ok end,
|
||||
try rocksdb:destroy_column_family(DbRef, CfH) catch error:_ -> ok end,
|
||||
delete_info(Alias, Tab, encoding_migration),
|
||||
NewLiveRef = clear_cf_migration(LiveRef),
|
||||
St1 = update_cf(Alias, Name, NewLiveRef, St),
|
||||
put_pt(Tab, NewLiveRef),
|
||||
{ok, St1};
|
||||
St2 = St1#st{migrators = maps:remove(Tab, St1#st.migrators)},
|
||||
{ok, St2};
|
||||
_ ->
|
||||
{error, no_migration}
|
||||
end.
|
||||
|
||||
clear_cf_migration(Cf) ->
|
||||
%% migration_meta is legacy on refs; progress/phase live in admin CF only.
|
||||
maps:without([migration, migration_meta, migration_epoch], Cf).
|
||||
|
||||
read_term(Str) ->
|
||||
|
||||
+8
-4
@@ -371,6 +371,10 @@ retry_activity(F, Alias, #{activity := #{ type := Type
|
||||
return_abort(Type, error, retry_limit)
|
||||
end.
|
||||
|
||||
%% Ctxt maps carry rocksdb opaque handles (tx | batch). Dialyzer reports
|
||||
%% Wopaque_union on try_f/2 when retry Ctxt (tx handle) is unified with the
|
||||
%% batch-activity Ctxt used on the first attempt via do_activity/3.
|
||||
-dialyzer({no_opaque, retry_activity_/4}).
|
||||
retry_activity_(inner, F, Alias, Ctxt) ->
|
||||
mrdb_stats:incr(Alias, inner_retries, 1),
|
||||
try_f(F, Ctxt);
|
||||
@@ -812,8 +816,8 @@ insert_(#{semantics := bag} = Ref, Key, EncKey, EncVal, Obj, Opts) ->
|
||||
%% insert_bag(Ref, Obj, Opts);
|
||||
insert_(Ref, Key, EncKey, EncVal, Obj, Opts) ->
|
||||
%% Close over Key/Obj so dual-write can re-encode into migration target CF.
|
||||
F = fun(R, EK, EV, _Ix, Os) ->
|
||||
rdb_put(R, EK, EV, Os),
|
||||
F = fun(R, EK, EV, Ix, Os) ->
|
||||
insert_set(R, EK, EV, Ix, Os),
|
||||
dual_put(R, Key, Obj, Os)
|
||||
end,
|
||||
batch_if_index(Ref, insert, set, F, Key, EncKey, EncVal, Obj, Opts).
|
||||
@@ -1281,8 +1285,8 @@ delete(Tab, Key, Opts) ->
|
||||
delete_(#{semantics := bag} = Ref, Key, EncKey, Opts) ->
|
||||
batch_if_index(Ref, delete, bag, fun delete_bag/5, Key, EncKey, [], [], Opts);
|
||||
delete_(Ref, Key, EncKey, Opts) ->
|
||||
F = fun(R, EK, _D, _Ix, Os) ->
|
||||
rdb_delete(R, EK, Os),
|
||||
F = fun(R, EK, D, Ix, Os) ->
|
||||
delete_set(R, EK, D, Ix, Os),
|
||||
dual_delete(R, Key, Os)
|
||||
end,
|
||||
batch_if_index(Ref, delete, set, F, Key, EncKey, [], [], Opts).
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
, 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").
|
||||
@@ -37,7 +41,11 @@ groups() ->
|
||||
, migrate_with_encoding_change
|
||||
, online_encoding_migration
|
||||
, online_encoding_migration_restart
|
||||
, online_encoding_migration_interrupt ]}
|
||||
, 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) ->
|
||||
@@ -238,6 +246,115 @@ online_encoding_migration_interrupt(_Config) ->
|
||||
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() ->
|
||||
|
||||
Reference in New Issue
Block a user