You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#20915 fixed a data-corruption bug where a CreateLookupIndex (lookup vindex
backfill) stream would follow a MigrationType_TABLES journal written by a
MoveTables SwitchTraffic/ReverseTraffic. The stream's filter selects keyspace_id() as <toCol>, which the vstreamer resolves via vschema.FindColVindex() against the serving tablet's local vschema
(go/vt/vttablet/tabletserver/vstreamer/planbuilder.go, case "keyspace_id"). When transitionJournal relocated the stream into the
MoveTables counterpart keyspace, that resolution either failed to plan (e.g.
no col vindex, unsharded source) or silently computed the wrong keyspace_id in a differently-sharded keyspace, corrupting the lookup
table. The fix, on the branch that produced this issue:
the vplayer's VEventType_JOURNAL handling now ignores TABLES journals
for CreateLookupIndex-typed streams and keeps replicating from the
stream's current source (correct, because the paired MoveTables workflow
keeps feeding that source after the switch);
registerJournal refuses to register a TABLES journal against a CreateLookupIndex controller at all, as insurance against any caller
that bypasses the vplayer gate.
Both of these gates are keyed on workflow_type == VReplicationWorkflowType_CreateLookupIndex. That leaves the same class of
bug open for Materialize workflows whose filter rules reference keyspace_id() or in_keyrange() — those functions have the identical
unexpressible-filter problem: a TABLES journal can relocate the stream
into a keyspace where the function no longer resolves to the same (or any)
value.
The type-based gate used for CreateLookupIndex cannot be reused as-is. Materialize = 0 is the zero value of VReplicationWorkflowType
(proto/binlogdata.proto), so pre-existing rows written before the workflow_type column existed — and any row that never set it — read
back as 0. Gating on workflow_type == Materialize would therefore
silently change behavior for every legacy stream that has nothing to do
with keyspace_id()/in_keyrange(), not just the ones with non-portable
filters (plain select * from t materializations are genuinely portable
and follow journals safely today). Any gate for this case has to be filter-content-based: inspect the stream's BinlogSource filter
rule(s) for references to keyspace_id()/in_keyrange(), independent of workflow_type.
Suggested approach
Content-based detection. Add a helper that walks a stream's filter
rules (binlogdatapb.BinlogSource.Filter.Rules[].Filter) and reports
whether any rule references keyspace_id() or in_keyrange(). This
likely means parsing/inspecting the filter expression the same way the
vstreamer planbuilder does when it recognizes these function names,
rather than a raw string match, to avoid both false negatives (aliased
or nested forms) and false positives (a column literally named keyspace_id). A stream with multiple rules — one per Materialize
target table — needs a decision on granularity: gate the whole
controller if any rule is affected (simplest, matches how the CreateLookupIndex gate treats the stream as a unit), or gate
per-rule if partial-follow is otherwise safe. Default to whole-stream
unless per-rule is shown to be needed.
Vplayer-side handling for the detected case. Decide the correct
behavior when a TABLES journal reaches a Materialize stream with a
non-portable filter. Unlike CreateLookupIndex, a Materialize
workflow has no general guarantee that a paired workflow keeps feeding
the stream's current source after the switch, so "ignore and keep
replicating from the current source" (the CreateLookupIndex fix's
choice) may not transfer directly and needs its own design/verification
before reuse.
Validate-before-destroy in transitionJournal (complementary
safety net, independent of the vplayer gate): before transitionJournal
deletes the old stream row(s) and inserts the replacement row(s)
pointed at the journal's keyspace/GTID, validate that any keyspace_id()/in_keyrange() filter rule can still resolve correctly
against the destination keyspace's vschema. If it can't, transitionJournal
must not proceed with the delete/recreate.
This has a failure-semantics constraint that any refusal must satisfy:
by the time transitionJournal runs, the participating controllers
have already been stopped (vre.controllers[refid].Stop(true)) in
anticipation of the delete+recreate. If a refusal simply returns and
leaves those streams in a plain stopped state, whatever normally
restarts a stopped stream (workflow resume, engine reopen, etc.) will
resume replication from the same position — which immediately
redelivers the same JOURNAL event and re-triggers the same refusal.
That's a stop/restart livelock, not a safe failure mode (this is the
same reasoning that ruled out "stop" as the CreateLookupIndex vplayer
response in favor of "ignore and continue" — see the plan/design notes
for Bug Report: LookupVindex after MoveTables ReverseTraffic retargets the lookup to the MoveTables source via a resharding journal #20915). A refusal in transitionJournal must instead durably park the affected stream(s): persist a Stopped state with a
clear, actionable message (e.g. naming the journal id and the reason
the filter can't be relocated) so the stream does not auto-resume into
the same failure, and so the condition is visible to
operators/monitoring rather than silently spinning.
Relationship between the two layers. Keep the existing CreateLookupIndex type-based gate as-is (cheap, specific, already
shipped). The content-based check is a broader, more expensive
safety net that should cover Materialize (and any other workflow type
that predates or falls outside the type-based gate) at the transitionJournal layer; whether it also needs a vplayer-side
fast path mirroring CreateLookupIndex's, or whether
validate-before-destroy alone is sufficient, is part of the design
work here.
VReplication: LookupVindex streams must not follow TABLES journals #20917 — adds the CreateLookupIndex vplayer gate and the registerJournal refusal described above; this issue covers the
deferred Materialize/content-based case and the transitionJournal
hardening called out in that PR's scope.
Overview
#20915 fixed a data-corruption bug where a
CreateLookupIndex(lookup vindexbackfill) stream would follow a
MigrationType_TABLESjournal written by aMoveTables
SwitchTraffic/ReverseTraffic. The stream's filter selectskeyspace_id() as <toCol>, which the vstreamer resolves viavschema.FindColVindex()against the serving tablet's local vschema(
go/vt/vttablet/tabletserver/vstreamer/planbuilder.go,case "keyspace_id"). WhentransitionJournalrelocated the stream into theMoveTables counterpart keyspace, that resolution either failed to plan (e.g.
no col vindex, unsharded source) or silently computed the wrong
keyspace_idin a differently-sharded keyspace, corrupting the lookuptable. The fix, on the branch that produced this issue:
VEventType_JOURNALhandling now ignoresTABLESjournalsfor
CreateLookupIndex-typed streams and keeps replicating from thestream's current source (correct, because the paired MoveTables workflow
keeps feeding that source after the switch);
registerJournalrefuses to register aTABLESjournal against aCreateLookupIndexcontroller at all, as insurance against any callerthat bypasses the vplayer gate.
Both of these gates are keyed on
workflow_type == VReplicationWorkflowType_CreateLookupIndex. That leaves the same class ofbug open for Materialize workflows whose filter rules reference
keyspace_id()orin_keyrange()— those functions have the identicalunexpressible-filter problem: a
TABLESjournal can relocate the streaminto a keyspace where the function no longer resolves to the same (or any)
value.
The type-based gate used for
CreateLookupIndexcannot be reused as-is.Materialize = 0is the zero value ofVReplicationWorkflowType(
proto/binlogdata.proto), so pre-existing rows written before theworkflow_typecolumn existed — and any row that never set it — readback as
0. Gating onworkflow_type == Materializewould thereforesilently change behavior for every legacy stream that has nothing to do
with
keyspace_id()/in_keyrange(), not just the ones with non-portablefilters (plain
select * from tmaterializations are genuinely portableand follow journals safely today). Any gate for this case has to be
filter-content-based: inspect the stream's
BinlogSourcefilterrule(s) for references to
keyspace_id()/in_keyrange(), independent ofworkflow_type.Suggested approach
Content-based detection. Add a helper that walks a stream's filter
rules (
binlogdatapb.BinlogSource.Filter.Rules[].Filter) and reportswhether any rule references
keyspace_id()orin_keyrange(). Thislikely means parsing/inspecting the filter expression the same way the
vstreamer planbuilder does when it recognizes these function names,
rather than a raw string match, to avoid both false negatives (aliased
or nested forms) and false positives (a column literally named
keyspace_id). A stream with multiple rules — one per Materializetarget table — needs a decision on granularity: gate the whole
controller if any rule is affected (simplest, matches how the
CreateLookupIndexgate treats the stream as a unit), or gateper-rule if partial-follow is otherwise safe. Default to whole-stream
unless per-rule is shown to be needed.
Vplayer-side handling for the detected case. Decide the correct
behavior when a
TABLESjournal reaches a Materialize stream with anon-portable filter. Unlike
CreateLookupIndex, a Materializeworkflow has no general guarantee that a paired workflow keeps feeding
the stream's current source after the switch, so "ignore and keep
replicating from the current source" (the
CreateLookupIndexfix'schoice) may not transfer directly and needs its own design/verification
before reuse.
Validate-before-destroy in
transitionJournal(complementarysafety net, independent of the vplayer gate): before
transitionJournaldeletes the old stream row(s) and inserts the replacement row(s)
pointed at the journal's keyspace/GTID, validate that any
keyspace_id()/in_keyrange()filter rule can still resolve correctlyagainst the destination keyspace's vschema. If it can't,
transitionJournalmust not proceed with the delete/recreate.
This has a failure-semantics constraint that any refusal must satisfy:
by the time
transitionJournalruns, the participating controllershave already been stopped (
vre.controllers[refid].Stop(true)) inanticipation of the delete+recreate. If a refusal simply returns and
leaves those streams in a plain stopped state, whatever normally
restarts a stopped stream (workflow resume, engine reopen, etc.) will
resume replication from the same position — which immediately
redelivers the same
JOURNALevent and re-triggers the same refusal.That's a stop/restart livelock, not a safe failure mode (this is the
same reasoning that ruled out "stop" as the
CreateLookupIndexvplayerresponse in favor of "ignore and continue" — see the plan/design notes
for Bug Report: LookupVindex after MoveTables ReverseTraffic retargets the lookup to the MoveTables source via a resharding journal #20915). A refusal in
transitionJournalmust instead durablypark the affected stream(s): persist a
Stoppedstate with aclear, actionable message (e.g. naming the journal id and the reason
the filter can't be relocated) so the stream does not auto-resume into
the same failure, and so the condition is visible to
operators/monitoring rather than silently spinning.
Relationship between the two layers. Keep the existing
CreateLookupIndextype-based gate as-is (cheap, specific, alreadyshipped). The content-based check is a broader, more expensive
safety net that should cover Materialize (and any other workflow type
that predates or falls outside the type-based gate) at the
transitionJournallayer; whether it also needs a vplayer-sidefast path mirroring
CreateLookupIndex's, or whethervalidate-before-destroy alone is sufficient, is part of the design
work here.
References
mcrauwel's comment proposing the validate-before-destroy shape).
CreateLookupIndexvplayer gate and theregisterJournalrefusal described above; this issue covers thedeferred Materialize/content-based case and the
transitionJournalhardening called out in that PR's scope.