Skip to content

VReplication: harden journal handling for Materialize workflows with non-portable filters #20918

Description

@mattlord

Overview

#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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions