WIP migrate table encoding
This commit is contained in:
+473
-22
@@ -19,7 +19,13 @@
|
||||
]).
|
||||
|
||||
-export([ migrate_standalone/2
|
||||
, migrate_standalone/3 ]).
|
||||
, migrate_standalone/3
|
||||
%% Online encoding migration (set tables): dual-write + CF copy
|
||||
, migrate_encoding/3
|
||||
, migrate_encoding/4
|
||||
, migration_status/1
|
||||
, finalize_migration/2
|
||||
]).
|
||||
|
||||
-export([ start_link/0
|
||||
, init/1
|
||||
@@ -49,6 +55,7 @@
|
||||
backends = #{} :: #{ alias() => backend() }
|
||||
, standalone = #{} :: #{{alias(), table()} := cf() }
|
||||
, default_opts = [] :: [{atom(), _}]
|
||||
, migrators = #{} :: #{ table() => pid() }
|
||||
}).
|
||||
|
||||
-type st() :: #st{}.
|
||||
@@ -79,6 +86,9 @@
|
||||
| {write_table_property, tabname(), tuple()}
|
||||
| {remove_aliases, [alias()]}
|
||||
| {migrate, [tabname() | {tabname(), map()}], rpt()}
|
||||
| {migrate_encoding, tabname(), any(), rpt()}
|
||||
| {migration_status, tabname()}
|
||||
| {finalize_migration, tabname()}
|
||||
| {prep_close, table()}
|
||||
| {close_table, table()}
|
||||
| {clear_table, table() | cf() }.
|
||||
@@ -289,6 +299,39 @@ migrate_standalone(Alias, Tabs, Rpt0) ->
|
||||
end,
|
||||
call(Alias, {migrate, Tabs, Rpt}).
|
||||
|
||||
%% @doc Start online encoding migration for a set table (dual-write + CF copy).
|
||||
-spec migrate_encoding(alias(), tabname(), Encoding :: any()) ->
|
||||
ok | {error, term()}.
|
||||
migrate_encoding(Alias, Tab, Encoding) ->
|
||||
migrate_encoding(Alias, Tab, Encoding, undefined).
|
||||
|
||||
-spec migrate_encoding(alias(), tabname(), Encoding :: any(), rpt()) ->
|
||||
ok | {error, term()}.
|
||||
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}).
|
||||
|
||||
-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
|
||||
#{} = Meta -> Meta;
|
||||
undefined -> #{phase => dual_write}
|
||||
end;
|
||||
_ ->
|
||||
idle
|
||||
end.
|
||||
|
||||
-spec finalize_migration(alias(), tabname()) -> ok | {error, term()}.
|
||||
finalize_migration(Alias, Tab) ->
|
||||
call(Alias, {finalize_migration, Tab}).
|
||||
|
||||
-spec call(alias() | [], req()) -> no_return() | any().
|
||||
call(Alias, Req) ->
|
||||
call(Alias, Req, infinity).
|
||||
@@ -451,6 +494,15 @@ handle_info({mnesia_table_event, Event}, St) ->
|
||||
_ ->
|
||||
{noreply, St}
|
||||
end;
|
||||
handle_info({'DOWN', _MRef, process, Pid, Reason},
|
||||
#st{migrators = Ms} = St) ->
|
||||
case lists:keyfind(Pid, 2, maps:to_list(Ms)) of
|
||||
{Tab, Pid} ->
|
||||
?log(info, "Encoding migrator for ~p exited: ~p", [Tab, Reason]),
|
||||
{noreply, St#st{migrators = maps:remove(Tab, Ms)}};
|
||||
false ->
|
||||
{noreply, St}
|
||||
end;
|
||||
handle_info(_Msg, St) ->
|
||||
{noreply, St}.
|
||||
|
||||
@@ -593,16 +645,36 @@ handle_req(Alias, {migrate, Tabs0, Rpt}, Backend, St) ->
|
||||
{reply, Res, St1};
|
||||
{error, _} = Error ->
|
||||
{reply, Error, St}
|
||||
end.
|
||||
end;
|
||||
handle_req(Alias, {migrate_encoding, Tab, Encoding, Rpt}, Backend, St) ->
|
||||
case start_encoding_migration(Alias, Tab, Encoding, Rpt, Backend, St) of
|
||||
{ok, St1} ->
|
||||
{reply, ok, St1};
|
||||
{error, _} = Error ->
|
||||
{reply, Error, St}
|
||||
end;
|
||||
handle_req(Alias, {finalize_migration, Tab}, Backend, St) ->
|
||||
case do_finalize_encoding_migration(Alias, Tab, Backend, St) of
|
||||
{ok, St1} ->
|
||||
{reply, ok, St1};
|
||||
{error, _} = Error ->
|
||||
{reply, Error, St}
|
||||
end;
|
||||
handle_req(_Alias, {migration_status, Tab}, _Backend, St) ->
|
||||
{reply, migration_status(Tab), St}.
|
||||
|
||||
handle_load_table_req(Alias, Name, TRec, Backend, St) ->
|
||||
case create_table_from_trec(Alias, Name, TRec, Backend, St) of
|
||||
{ok, TRec1, St1} ->
|
||||
TRec2 = TRec1#{status => open},
|
||||
St2 = update_cf(Alias, Name, TRec2, St1),
|
||||
Backend1 = maps:get(Alias, St1#st.backends, Backend),
|
||||
{TRec3, St2} =
|
||||
maybe_resume_encoding_migration(
|
||||
Alias, Name, TRec2, Backend1, St1),
|
||||
St3 = update_cf(Alias, Name, TRec3, St2),
|
||||
?log(debug, "Table loaded ~p", [Name]),
|
||||
put_pt(Name, TRec2),
|
||||
{reply, {ok, TRec2}, St2};
|
||||
put_pt(Name, TRec3),
|
||||
{reply, {ok, TRec3}, St3};
|
||||
{error, _} = Error ->
|
||||
{reply, Error, St}
|
||||
end.
|
||||
@@ -1158,8 +1230,11 @@ create_table_as_cf(Alias, Name, #{db_ref := DbRef} = R, St) ->
|
||||
CfName = tab_to_cf_name(Name),
|
||||
case create_column_family(DbRef, CfName, cfopts(), R) of
|
||||
{ok, CfH} ->
|
||||
R1 = check_version_and_encoding(R#{ cf_handle => CfH
|
||||
, type => column_family }),
|
||||
R1 = check_version_and_encoding(
|
||||
R#{ cf_handle => CfH
|
||||
, type => column_family
|
||||
, cf_gen => maps:get(cf_gen, R, 0)
|
||||
, cf_name => CfName }),
|
||||
{ok, R1, update_cf(Alias, Name, R1, St)};
|
||||
{error, _} = Error ->
|
||||
Error
|
||||
@@ -1363,37 +1438,413 @@ admin_cfs({info, _} = I) -> [ {tab_to_cf_name(I), cfopts()} ].
|
||||
|
||||
map_cfs({ok, Ref, CfHandles}, CFs, Alias, Acc) ->
|
||||
ZippedCFs = lists:zip(CFs, CfHandles),
|
||||
%% io:fwrite("ZippedCFs = ~p~n", [ZippedCFs]),
|
||||
CfInfo = maps:from_list(
|
||||
[{cf_name_to_tab(N, Alias), #{ db_ref => Ref
|
||||
, cf_handle => H
|
||||
, alias => Alias
|
||||
, status => pre_existing
|
||||
, type => column_family }}
|
||||
|| {{N,_}, H} <- ZippedCFs]),
|
||||
%% Group versioned data CFs by logical tab and generation. cf_info keeps one
|
||||
%% provisional live handle per logical name (highest gen); full map is in
|
||||
%% data_cf_by_gen for migration resume (source gen may be lower than target).
|
||||
{CfInfo, ByGen} =
|
||||
lists:foldl(
|
||||
fun({{N, _}, H}, {Map, BG}) ->
|
||||
Logical = cf_name_to_tab(N, Alias),
|
||||
Gen = case cf_name_to_data_gen(N) of
|
||||
{ok, _, G} -> G;
|
||||
error -> 0
|
||||
end,
|
||||
Rec = #{ db_ref => Ref
|
||||
, cf_handle => H
|
||||
, alias => Alias
|
||||
, status => pre_existing
|
||||
, type => column_family
|
||||
, cf_gen => Gen
|
||||
, cf_name => N },
|
||||
BG1 = case is_atom(Logical) of
|
||||
true ->
|
||||
GMap0 = maps:get(Logical, BG, #{}),
|
||||
BG#{Logical => GMap0#{Gen => Rec}};
|
||||
false ->
|
||||
BG
|
||||
end,
|
||||
Map1 = case maps:find(Logical, Map) of
|
||||
{ok, #{cf_gen := OldG}}
|
||||
when is_integer(OldG), OldG > Gen ->
|
||||
Map;
|
||||
_ ->
|
||||
Map#{Logical => Rec}
|
||||
end,
|
||||
{Map1, BG1}
|
||||
end, {#{}, #{}}, ZippedCFs),
|
||||
{ok, Acc#{ db_ref => Ref
|
||||
, cf_info => CfInfo }}.
|
||||
, cf_info => CfInfo
|
||||
, data_cf_by_gen => ByGen }}.
|
||||
|
||||
tab_to_cf_name(Tab) when is_atom(Tab) -> write_term({d, Tab});
|
||||
%% Column-family naming
|
||||
%% ---------------------
|
||||
%% RocksDB requires unique CF names within a DB. Logical table names (atoms)
|
||||
%% are not enough once we rewrite a table into a new CF (encoding migration):
|
||||
%% the old and new CFs must coexist until cutover.
|
||||
%%
|
||||
%% Data CFs are versioned:
|
||||
%% gen 0 (legacy / first create): "{d, Tab}"
|
||||
%% gen N (N >= 1): "{d, Tab, N}"
|
||||
%%
|
||||
%% The live generation is stored on the db_ref as `cf_gen` and durably in
|
||||
%% admin info `{cf_gen, Tab}`. `cf_name_to_tab/2` maps any generation back to
|
||||
%% the logical table name for open/recovery; when several gens exist, the
|
||||
%% recorded live gen wins (see map_cfs / resolve_data_cf).
|
||||
%%
|
||||
%% Index / retainer / admin CFs are unchanged: one logical resource ↔ one CF.
|
||||
|
||||
tab_to_cf_name(Tab) when is_atom(Tab) ->
|
||||
data_cf_name(Tab, 0);
|
||||
tab_to_cf_name({admin, Alias}) -> write_term({a, Alias});
|
||||
tab_to_cf_name({info, Tab}) -> write_term({n, Tab});
|
||||
tab_to_cf_name({Tab, index, I}) -> write_term({i, Tab, I});
|
||||
tab_to_cf_name({Tab, retainer, R}) -> write_term({r, Tab, R}).
|
||||
|
||||
%% Physical CF name for a data table generation.
|
||||
data_cf_name(Tab, 0) when is_atom(Tab) ->
|
||||
write_term({d, Tab});
|
||||
data_cf_name(Tab, Gen) when is_atom(Tab), is_integer(Gen), Gen > 0 ->
|
||||
write_term({d, Tab, Gen}).
|
||||
|
||||
write_term(T) ->
|
||||
lists:flatten(io_lib:fwrite("~w", [T])).
|
||||
|
||||
cf_name_to_tab(Cf, Alias) ->
|
||||
case read_term(Cf) of
|
||||
{ok, {d, Table}} -> Table;
|
||||
{ok, {i, Table, I}} -> {Table, index, I};
|
||||
{ok, {r, Table, R}} -> {Table, retainer, R};
|
||||
{ok, {n, Table}} -> {info, Table};
|
||||
{ok, {a, Alias}} -> {admin, Alias};
|
||||
{ok, {d, Table}} -> Table;
|
||||
{ok, {d, Table, _Gen}} -> Table; %% versioned data CF → logical tab
|
||||
{ok, {i, Table, I}} -> {Table, index, I};
|
||||
{ok, {r, Table, R}} -> {Table, retainer, R};
|
||||
{ok, {n, Table}} -> {info, Table};
|
||||
{ok, {a, Alias}} -> {admin, Alias};
|
||||
_ ->
|
||||
{ext, Alias, Cf}
|
||||
end.
|
||||
|
||||
%% Parse generation from a data CF name string; non-data → error.
|
||||
cf_name_to_data_gen(Cf) ->
|
||||
case read_term(Cf) of
|
||||
{ok, {d, Table}} -> {ok, Table, 0};
|
||||
{ok, {d, Table, Gen}} -> {ok, Table, Gen};
|
||||
_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
%% =====================================================================
|
||||
%% Online encoding migration (set tables)
|
||||
%% =====================================================================
|
||||
|
||||
start_encoding_migration(Alias, Tab, Encoding0, Rpt, Backend, St)
|
||||
when is_atom(Tab) ->
|
||||
case find_cf(Alias, Tab, Backend, St) of
|
||||
{ok, #{status := open, semantics := bag}} ->
|
||||
{error, bag_not_supported};
|
||||
{ok, #{status := open, migration := _}} ->
|
||||
{error, already_migrating};
|
||||
{ok, #{status := open} = OldRef} ->
|
||||
case maps:get(type, OldRef, column_family) of
|
||||
standalone ->
|
||||
{error, standalone_not_supported};
|
||||
column_family ->
|
||||
start_encoding_migration_(Alias, Tab, Encoding0, Rpt,
|
||||
OldRef, Backend, St)
|
||||
end;
|
||||
{ok, _} ->
|
||||
{error, not_open};
|
||||
error ->
|
||||
{error, not_found}
|
||||
end;
|
||||
start_encoding_migration(_, _, _, _, _, _) ->
|
||||
{error, badarg}.
|
||||
|
||||
start_encoding_migration_(Alias, Tab, Encoding0, Rpt, OldRef, Backend, St) ->
|
||||
As = maps:get(attributes,
|
||||
maps:get(properties, OldRef, #{}),
|
||||
[key, val]),
|
||||
case mnesia_rocksdb_lib:check_encoding(Encoding0, As) of
|
||||
{ok, NewEnc} ->
|
||||
case maps:get(encoding, OldRef) of
|
||||
NewEnc ->
|
||||
{error, same_encoding};
|
||||
_OldEnc ->
|
||||
create_and_start_encoding_mig(
|
||||
Alias, Tab, NewEnc, Rpt, OldRef, Backend, St)
|
||||
end;
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
create_and_start_encoding_mig(Alias, Tab, NewEnc, Rpt, OldRef,
|
||||
#{db_ref := DbRef} = _Backend, St) ->
|
||||
OldGen = maps:get(cf_gen, OldRef, 0),
|
||||
NewGen = OldGen + 1,
|
||||
CfName = data_cf_name(Tab, NewGen),
|
||||
case create_column_family(DbRef, CfName, cfopts(), OldRef) of
|
||||
{ok, CfH} ->
|
||||
%% Target ref: same DB, new versioned CF, new encoding; no migration field.
|
||||
NewRef0 = maps:without(
|
||||
[migration, migration_meta, migration_epoch],
|
||||
OldRef),
|
||||
NewRef0b = NewRef0#{ cf_handle => CfH
|
||||
, encoding => NewEnc
|
||||
, cf_gen => NewGen
|
||||
, cf_name => CfName
|
||||
, name => Tab
|
||||
, type => column_family
|
||||
, status => open },
|
||||
%% Persist encoding in user_properties *before* check_version_and_encoding
|
||||
%% so it is not replaced by the table default.
|
||||
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}
|
||||
, source_gen => OldGen
|
||||
, cursor => '$first'
|
||||
, epoch => 1
|
||||
, started_at => erlang:system_time(millisecond) },
|
||||
%% Live ref: dual-write to NewRef; reads still use Old CF handle.
|
||||
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),
|
||||
{Pid, _MRef} = spawn_monitor(
|
||||
fun() ->
|
||||
encoding_migrator(Alias, Tab, LiveRef2,
|
||||
NewRef, Rpt)
|
||||
end),
|
||||
rpt(Rpt, "Started encoding migration ~p gen ~p -> ~p (~s)~n",
|
||||
[Tab, OldGen, NewGen, CfName]),
|
||||
{ok, St1#st{migrators = maps:put(Tab, Pid, St#st.migrators)}};
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
%% Re-attach dual-write (and copier) after restart when durable meta says so.
|
||||
maybe_resume_encoding_migration(Alias, Name, LiveRef, Backend, St)
|
||||
when is_atom(Name) ->
|
||||
case read_info(Alias, Name, encoding_migration, undefined) of
|
||||
#{phase := Phase, target := #{encoding := Enc, cf_gen := NewGen}} = Meta
|
||||
when Phase =:= copying; Phase =:= copy_done ->
|
||||
SourceGen = maps:get(source_gen, Meta, NewGen - 1),
|
||||
ByGen = maps:get(data_cf_by_gen, Backend, #{}),
|
||||
TabGens = maps:get(Name, ByGen, #{}),
|
||||
case {maps:find(SourceGen, TabGens), maps:find(NewGen, TabGens)} of
|
||||
{{ok, SrcRec}, {ok, TgtRec}} ->
|
||||
resume_encoding_migration_(
|
||||
Alias, Name, LiveRef, Meta, Enc, NewGen, SourceGen,
|
||||
SrcRec, TgtRec, Phase, St);
|
||||
_ ->
|
||||
?log(warning,
|
||||
"encoding migration meta for ~p but CFs missing "
|
||||
"(source=~p target=~p gens=~p)",
|
||||
[Name, SourceGen, NewGen, maps:keys(TabGens)]),
|
||||
{LiveRef, St}
|
||||
end;
|
||||
_ ->
|
||||
%% Completed migration: ensure live handle matches durable cf_gen.
|
||||
maybe_align_cf_gen(Alias, Name, LiveRef, Backend, St)
|
||||
end;
|
||||
maybe_resume_encoding_migration(_, _, LiveRef, _, St) ->
|
||||
{LiveRef, St}.
|
||||
|
||||
maybe_align_cf_gen(Alias, Name, LiveRef, Backend, St) ->
|
||||
case read_info(Alias, Name, cf_gen, undefined) of
|
||||
Gen when is_integer(Gen) ->
|
||||
ByGen = maps:get(data_cf_by_gen, Backend, #{}),
|
||||
case maps:find(Gen, maps:get(Name, ByGen, #{})) of
|
||||
{ok, Rec} ->
|
||||
Live1 = maps:merge(
|
||||
LiveRef,
|
||||
maps:with([cf_handle, cf_gen, cf_name, db_ref],
|
||||
Rec)),
|
||||
{Live1, St};
|
||||
error ->
|
||||
{LiveRef, St}
|
||||
end;
|
||||
_ ->
|
||||
{LiveRef, St}
|
||||
end.
|
||||
|
||||
resume_encoding_migration_(Alias, Name, LiveRef, Meta, Enc, NewGen, SourceGen,
|
||||
SrcRec, TgtRec, Phase, St) ->
|
||||
%% Live reads use source gen CF + old encoding (from LiveRef / schema).
|
||||
Live0 = maps:merge(
|
||||
LiveRef,
|
||||
maps:with([cf_handle, cf_name, db_ref], SrcRec)),
|
||||
Live1 = Live0#{cf_gen => SourceGen, status => open},
|
||||
NewRef0 = maps:without(
|
||||
[migration, migration_meta, migration_epoch], LiveRef),
|
||||
NewRef = NewRef0#{ cf_handle => maps:get(cf_handle, TgtRec)
|
||||
, cf_name => maps:get(cf_name, TgtRec)
|
||||
, cf_gen => NewGen
|
||||
, encoding => Enc
|
||||
, db_ref => maps:get(db_ref, TgtRec)
|
||||
, name => Name
|
||||
, status => open },
|
||||
NewRef1 = update_user_properties({mrdb_encoding, Enc}, NewRef),
|
||||
NewRef2 = NewRef1#{encoding => Enc},
|
||||
Meta1 = Meta#{phase => Phase},
|
||||
Live2 = Live1#{ migration => NewRef2
|
||||
, migration_meta => Meta1
|
||||
, migration_epoch => maps:get(epoch, Meta, 1) },
|
||||
put_pt(Name, Live2),
|
||||
St1 = case Phase of
|
||||
copying ->
|
||||
case maps:get(Name, St#st.migrators, undefined) of
|
||||
Pid when is_pid(Pid) ->
|
||||
St;
|
||||
_ ->
|
||||
{Pid, _} = spawn_monitor(
|
||||
fun() ->
|
||||
encoding_migrator(
|
||||
Alias, Name, Live2, NewRef2,
|
||||
undefined)
|
||||
end),
|
||||
St#st{migrators = maps:put(Name, Pid, St#st.migrators)}
|
||||
end;
|
||||
copy_done ->
|
||||
St
|
||||
end,
|
||||
{Live2, St1}.
|
||||
|
||||
%% Background copier: walk old CF, install into new if missing (no clobber).
|
||||
encoding_migrator(Alias, Tab, OldRef, NewRef, Rpt) ->
|
||||
try
|
||||
Chunk = 500,
|
||||
N = encoding_copy_loop(OldRef, NewRef, '$first', 0, Chunk, Rpt),
|
||||
Meta = #{ phase => copy_done
|
||||
, target => #{encoding => maps:get(encoding, NewRef)}
|
||||
, 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 migration copy done for ~p (~p objs)~n", [Tab, N]),
|
||||
ok
|
||||
catch
|
||||
C:R:ST ->
|
||||
?log(error, "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)
|
||||
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'}
|
||||
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'}
|
||||
end;
|
||||
encoding_select_chunk(OldRef, _Other, Limit) ->
|
||||
%% Fallback: restart from first (safe, may re-process; install is idempotent).
|
||||
encoding_select_chunk(OldRef, '$first', Limit).
|
||||
|
||||
encoding_maybe_install(OldRef, NewRef, Obj) ->
|
||||
Name = maps:get(name, OldRef),
|
||||
KP = mnesia_rocksdb_lib:keypos(Name),
|
||||
Key = element(KP, Obj),
|
||||
OldOnly = maps:without(
|
||||
[migration, migration_meta, migration_epoch], OldRef),
|
||||
%% Live re-check on old (source of truth for reads during migration).
|
||||
case mrdb:read(OldOnly, Key) of
|
||||
[] ->
|
||||
ok; %% deleted after we observed Obj
|
||||
[LiveObj] ->
|
||||
case mrdb:read(NewRef, Key) of
|
||||
[_] ->
|
||||
ok; %% dual-write already installed
|
||||
[] ->
|
||||
mrdb:insert(NewRef, LiveObj)
|
||||
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, #{}),
|
||||
case maps:get(phase, Meta, undefined) of
|
||||
copy_done ->
|
||||
finalize_encoding_migration_(Alias, Tab, LiveRef, MigRef, St);
|
||||
Phase ->
|
||||
{error, {not_ready, Phase}}
|
||||
end;
|
||||
error ->
|
||||
{error, not_found};
|
||||
_ ->
|
||||
{error, not_migrating}
|
||||
end.
|
||||
|
||||
%% 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) ->
|
||||
#{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),
|
||||
LiveNew = maps:without(
|
||||
[migration, migration_meta, migration_epoch],
|
||||
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.
|
||||
write_info(Alias, Tab, cf_gen, NewGen),
|
||||
delete_info(Alias, Tab, encoding_migration),
|
||||
_ = maybe_write_user_props(LiveNew1),
|
||||
%% Drop previous generation CF (by handle; name may be {d,Tab} or {d,Tab,G}).
|
||||
ok = rocksdb:drop_column_family(DbRef, OldCfH),
|
||||
catch rocksdb:destroy_column_family(DbRef, OldCfH),
|
||||
drop_cached_cf(maps:get(cf_name, LiveRef,
|
||||
data_cf_name(Tab, maps:get(cf_gen, LiveRef, 0))),
|
||||
OldCfH),
|
||||
St2 = St1#st{migrators = maps:remove(Tab, St1#st.migrators)},
|
||||
{ok, St2}.
|
||||
|
||||
read_term(Str) ->
|
||||
case erl_scan:string(Str) of
|
||||
{ok, Tokens, _} ->
|
||||
|
||||
+29
-6
@@ -811,8 +811,12 @@ insert_(#{semantics := bag} = Ref, Key, EncKey, EncVal, Obj, Opts) ->
|
||||
batch_if_index(Ref, insert, bag, fun insert_bag/5, Key, EncKey, EncVal, Obj, Opts);
|
||||
%% insert_bag(Ref, Obj, Opts);
|
||||
insert_(Ref, Key, EncKey, EncVal, Obj, Opts) ->
|
||||
batch_if_index(Ref, insert, set, fun insert_set/5, Key, EncKey, EncVal, Obj, Opts).
|
||||
%% insert_set(Ref, 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),
|
||||
dual_put(R, Key, Obj, Os)
|
||||
end,
|
||||
batch_if_index(Ref, insert, set, F, Key, EncKey, EncVal, Obj, Opts).
|
||||
|
||||
insert_set(Ref, EncKey, EncVal, _, Opts) ->
|
||||
rdb_put(Ref, EncKey, EncVal, Opts).
|
||||
@@ -822,6 +826,19 @@ insert_bag(Ref, EncKey, EncVal, _, Opts) ->
|
||||
%% #{vsn := 1} ->
|
||||
insert_bag_v1(Ref, EncKey, EncVal, Opts).
|
||||
|
||||
%% During online encoding migration, mirror set put/delete onto the target CF.
|
||||
dual_put(#{migration := Mig0} = R, Key, Obj, Opts) ->
|
||||
Mig = maps:merge(Mig0, maps:with([activity, snapshot], R)),
|
||||
rdb_put(Mig, encode_key(Key, Mig), encode_val(Obj, Mig), Opts);
|
||||
dual_put(_, _, _, _) ->
|
||||
ok.
|
||||
|
||||
dual_delete(#{migration := Mig0} = R, Key, Opts) ->
|
||||
Mig = maps:merge(Mig0, maps:with([activity, snapshot], R)),
|
||||
rdb_delete(Mig, encode_key(Key, Mig), Opts);
|
||||
dual_delete(_, _, _) ->
|
||||
ok.
|
||||
|
||||
batch_if_index(#{mode := mnesia} = Ref, _, _, F, _Key, EncKey, Data, _Obj, Opts) ->
|
||||
F(Ref, EncKey, Data, undefined, Opts);
|
||||
batch_if_index(#{name := Name, properties := #{index := [_|_] = Ixs}} = Ref,
|
||||
@@ -1264,7 +1281,11 @@ 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) ->
|
||||
batch_if_index(Ref, delete, set, fun delete_set/5, Key, EncKey, [], [], Opts).
|
||||
F = fun(R, EK, _D, _Ix, Os) ->
|
||||
rdb_delete(R, EK, Os),
|
||||
dual_delete(R, Key, Os)
|
||||
end,
|
||||
batch_if_index(Ref, delete, set, F, Key, EncKey, [], [], Opts).
|
||||
|
||||
delete_object(Tab, Obj) ->
|
||||
delete_object(Tab, Obj, []).
|
||||
@@ -1598,15 +1619,17 @@ do_del_obj_bag_(Sz, K, Res, Obj, #{name := Name} = Ref, I, Opts) ->
|
||||
|
||||
delete_obj_set(_, _, _, not_found, _) ->
|
||||
ok;
|
||||
delete_obj_set(Ref, _, _, RawKey, Opts) when is_binary(RawKey) ->
|
||||
rdb_delete(Ref, RawKey, Opts);
|
||||
delete_obj_set(#{name := Name} = Ref, _, Obj, RawKey, Opts) when is_binary(RawKey) ->
|
||||
rdb_delete(Ref, RawKey, Opts),
|
||||
dual_delete(Ref, element(keypos(Name), Obj), Opts);
|
||||
delete_obj_set(#{name := Name} = Ref, EncKey, Obj, _, Opts) ->
|
||||
case rdb_get(Ref, EncKey, []) of
|
||||
{ok, Bin} ->
|
||||
Key = element(keypos(Name), Obj),
|
||||
case decode_val(Bin, Key, Ref) of
|
||||
Obj ->
|
||||
rdb_delete(Ref, EncKey, Opts);
|
||||
rdb_delete(Ref, EncKey, Opts),
|
||||
dual_delete(Ref, Key, Opts);
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user