Skip to content

Commit 5220045

Browse files
[release-24.0] VReplication: LookupVindex streams must not follow TABLES journals (#20917) (#20937)
Signed-off-by: Matt Lord <mattalord@gmail.com> Co-authored-by: vitess-bot[bot] <108069721+vitess-bot[bot]@users.noreply.github.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 093f341 commit 5220045

4 files changed

Lines changed: 181 additions & 2 deletions

File tree

go/vt/vttablet/tabletmanager/vreplication/engine.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,15 @@ func (vre *Engine) registerJournal(journal *binlogdatapb.Journal, id int32) erro
582582
}
583583

584584
workflow := vre.controllers[id].workflow
585+
// Insurance against callers that bypass the vplayer's gate (see the
586+
// VEventType_JOURNAL handling): lookup vindex backfill streams must
587+
// never be transitioned by a TABLES journal, as their keyspace_id()
588+
// filter is not expressible in another keyspace. transitionJournal
589+
// would otherwise irrecoverably delete the streams.
590+
if journal.MigrationType == binlogdatapb.MigrationType_TABLES &&
591+
binlogdatapb.VReplicationWorkflowType(vre.controllers[id].workflowType) == binlogdatapb.VReplicationWorkflowType_CreateLookupIndex {
592+
return vterrors.Errorf(vtrpcpb.Code_FAILED_PRECONDITION, "cannot follow TABLES journal %d for lookup vindex workflow %s: lookup backfill streams cannot be retargeted to another keyspace", journal.Id, workflow)
593+
}
585594
key := fmt.Sprintf("%s:%d", workflow, journal.Id)
586595
ks := fmt.Sprintf("%s:%s", vre.controllers[id].source.Keyspace, vre.controllers[id].source.Shard)
587596
log.Info(fmt.Sprintf("Journal encountered for (%s %s): %v", key, ks, journal))

go/vt/vttablet/tabletmanager/vreplication/journal_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import (
2020
"fmt"
2121
"testing"
2222

23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
2326
binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
2427
qh "vitess.io/vitess/go/vt/vttablet/tabletmanager/vreplication/queryhistory"
2528
)
@@ -314,3 +317,140 @@ func TestJournalTableMixed(t *testing.T) {
314317
deleteAllVReplicationStreams(t)
315318
expectDeleteQueries(t)
316319
}
320+
321+
// TestJournalTablesLookupVindexIgnored guards against resharding journals
322+
// written by MoveTables SwitchTraffic/ReverseTraffic destroying lookup
323+
// vindex backfill streams (https://github.com/vitessio/vitess/issues/20915).
324+
// A CreateLookupIndex stream must ignore a TABLES journal -- its
325+
// keyspace_id() filter cannot be planned in another keyspace -- and keep
326+
// replicating from its current source, which the paired workflow keeps
327+
// feeding after the switch. SHARDS (Reshard) journals are still followed;
328+
// that behavior is pinned by TestJournalOneToOne/TestJournalOneToMany.
329+
func TestJournalTablesLookupVindexIgnored(t *testing.T) {
330+
if runNoBlobTest {
331+
t.Skip("CreateLookupIndex workflows do not support binlog_row_image=noblob")
332+
}
333+
// This test's teardown uses defer, not t.Cleanup: execStatements and
334+
// the other framework helpers run on t.Context(), which is already
335+
// canceled by the time t.Cleanup callbacks execute.
336+
defer deleteTablet(addTablet(100))
337+
defer deleteTablet(addOtherTablet(101, "other_keyspace", "0"))
338+
339+
execStatements(t, []string{
340+
"create table t(id int, val varbinary(128), primary key(id))",
341+
fmt.Sprintf("create table %s.t(id int, val varbinary(128), primary key(id))", vrepldb),
342+
})
343+
defer execStatements(t, []string{
344+
"drop table t",
345+
fmt.Sprintf("drop table %s.t", vrepldb),
346+
})
347+
348+
filter := &binlogdatapb.Filter{
349+
Rules: []*binlogdatapb.Rule{{
350+
Match: "t",
351+
}},
352+
}
353+
bls := &binlogdatapb.BinlogSource{
354+
Keyspace: env.KeyspaceName,
355+
Shard: env.ShardName,
356+
Filter: filter,
357+
OnDdl: binlogdatapb.OnDDLAction_IGNORE,
358+
}
359+
cancel, _ := startVReplicationWithWorkflowType(t, bls, "", binlogdatapb.VReplicationWorkflowType_CreateLookupIndex)
360+
defer cancel()
361+
362+
journal := &binlogdatapb.Journal{
363+
Id: 1,
364+
MigrationType: binlogdatapb.MigrationType_TABLES,
365+
Participants: []*binlogdatapb.KeyspaceShard{{
366+
Keyspace: "vttest",
367+
Shard: "0",
368+
}},
369+
Tables: []string{"t"},
370+
ShardGtids: []*binlogdatapb.ShardGtid{{
371+
Keyspace: "other_keyspace",
372+
Shard: "0",
373+
Gtid: "MySQL56/7b04699f-f5e9-11e9-bf88-9cb6d089e1c3:1-10",
374+
}},
375+
}
376+
query := fmt.Sprintf("insert into _vt.resharding_journal(id, db_name, val) values (1, 'vttest', %v)", encodeString(journal.String()))
377+
execStatements(t, []string{query})
378+
defer execStatements(t, []string{"delete from _vt.resharding_journal"})
379+
380+
// The journal must be ignored: no stream deletion/recreation. The
381+
// stream must still be replicating from the original source, which
382+
// this insert proves end to end.
383+
execStatements(t, []string{"insert into t values(1, 'aaa')"})
384+
expectDBClientQueries(t, qh.Expect(
385+
"begin",
386+
"insert into t(id,val) values (1,_binary'aaa')",
387+
"/update _vt.vreplication set pos=",
388+
"commit",
389+
))
390+
}
391+
392+
// TestJournalRegisterLookupVindexRefused guards the engine-side insurance
393+
// for https://github.com/vitessio/vitess/issues/20915: even if a future
394+
// caller bypasses the vplayer's TABLES-journal gate, registerJournal must
395+
// refuse to transition a lookup vindex workflow rather than let
396+
// transitionJournal destroy its streams.
397+
func TestJournalRegisterLookupVindexRefused(t *testing.T) {
398+
if runNoBlobTest {
399+
t.Skip("CreateLookupIndex workflows do not support binlog_row_image=noblob")
400+
}
401+
// This test's teardown uses defer, not t.Cleanup: execStatements and
402+
// the other framework helpers run on t.Context(), which is already
403+
// canceled by the time t.Cleanup callbacks execute.
404+
defer deleteTablet(addTablet(100))
405+
defer deleteTablet(addOtherTablet(101, "other_keyspace", "0"))
406+
407+
execStatements(t, []string{
408+
"create table t(id int, val varbinary(128), primary key(id))",
409+
fmt.Sprintf("create table %s.t(id int, val varbinary(128), primary key(id))", vrepldb),
410+
})
411+
defer execStatements(t, []string{
412+
"drop table t",
413+
fmt.Sprintf("drop table %s.t", vrepldb),
414+
})
415+
416+
filter := &binlogdatapb.Filter{
417+
Rules: []*binlogdatapb.Rule{{
418+
Match: "t",
419+
}},
420+
}
421+
bls := &binlogdatapb.BinlogSource{
422+
Keyspace: env.KeyspaceName,
423+
Shard: env.ShardName,
424+
Filter: filter,
425+
OnDdl: binlogdatapb.OnDDLAction_IGNORE,
426+
}
427+
cancel, id := startVReplicationWithWorkflowType(t, bls, "", binlogdatapb.VReplicationWorkflowType_CreateLookupIndex)
428+
defer cancel()
429+
430+
journal := &binlogdatapb.Journal{
431+
Id: 2,
432+
MigrationType: binlogdatapb.MigrationType_TABLES,
433+
Participants: []*binlogdatapb.KeyspaceShard{{
434+
Keyspace: "vttest",
435+
Shard: "0",
436+
}},
437+
Tables: []string{"t"},
438+
ShardGtids: []*binlogdatapb.ShardGtid{{
439+
Keyspace: "other_keyspace",
440+
Shard: "0",
441+
Gtid: "MySQL56/7b04699f-f5e9-11e9-bf88-9cb6d089e1c3:1-10",
442+
}},
443+
}
444+
445+
err := playerEngine.registerJournal(journal, int32(id))
446+
require.ErrorContains(t, err, "lookup vindex")
447+
448+
// The stream must be untouched: no journaler entry queued, no
449+
// transition, the controller still registered.
450+
playerEngine.mu.Lock()
451+
_, ok := playerEngine.controllers[int32(id)]
452+
journalerEmpty := len(playerEngine.journaler) == 0
453+
playerEngine.mu.Unlock()
454+
assert.True(t, ok, "controller was removed")
455+
assert.True(t, journalerEmpty, "journaler entry was created")
456+
}

go/vt/vttablet/tabletmanager/vreplication/vplayer.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,29 @@ func (vp *vplayer) applyEvent(ctx context.Context, event *binlogdatapb.VEvent, m
852852
case binlogdatapb.MigrationType_SHARDS:
853853
// All tables of the source were migrated. So, no validation needed.
854854
case binlogdatapb.MigrationType_TABLES:
855+
// Lookup vindex backfill streams must never follow a TABLES
856+
// journal (written by MoveTables SwitchTraffic/ReverseTraffic):
857+
// their filter selects keyspace_id(), which the vstreamer
858+
// resolves against the serving keyspace's vschema, so a
859+
// relocated stream either fails to plan or computes wrong
860+
// keyspace_id values and corrupts the lookup table. The
861+
// stream's current source keeps receiving the owner table's
862+
// writes via the paired workflow after the switch -- when
863+
// reverse replication is running, which is the default -- so
864+
// we ignore the journal and keep replicating from it. With
865+
// --enable-reverse-replication=false nothing feeds the
866+
// current source and the backfill goes stale until reverse
867+
// replication is started or the switch is reversed. SHARDS
868+
// (Reshard) journals are still followed: the keyspace -- and
869+
// with it the filter's validity -- does not change.
870+
if binlogdatapb.VReplicationWorkflowType(vp.vr.WorkflowType) == binlogdatapb.VReplicationWorkflowType_CreateLookupIndex {
871+
log.Info("Ignoring TABLES journal for lookup vindex workflow",
872+
slog.String("workflow", vp.vr.WorkflowName),
873+
slog.Int64("journal_id", event.Journal.Id),
874+
slog.Any("journal_tables", event.Journal.Tables),
875+
)
876+
return nil
877+
}
855878
// Validate that all or none of the tables are in the journal.
856879
jtables := make(map[string]bool)
857880
for _, table := range event.Journal.Tables {

go/vt/vttablet/tabletmanager/vreplication/vplayer_flaky_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4230,12 +4230,19 @@ func expectJSON(t *testing.T, table string, values [][]string, id int, exec func
42304230

42314231
func startVReplication(t *testing.T, bls *binlogdatapb.BinlogSource, pos string) (cancelFunc func(), id int) {
42324232
t.Helper()
4233+
// fake workflow type as MoveTables so that we can test with "noblob" binlog row image
4234+
return startVReplicationWithWorkflowType(t, bls, pos, binlogdatapb.VReplicationWorkflowType_MoveTables)
4235+
}
4236+
4237+
func startVReplicationWithWorkflowType(t *testing.T, bls *binlogdatapb.BinlogSource, pos string,
4238+
workflowType binlogdatapb.VReplicationWorkflowType,
4239+
) (cancelFunc func(), id int) {
4240+
t.Helper()
42334241

42344242
if pos == "" {
42354243
pos = primaryPosition(t)
42364244
}
4237-
// fake workflow type as MoveTables so that we can test with "noblob" binlog row image
4238-
query := binlogplayer.CreateVReplication("test", bls, pos, 9223372036854775807, 9223372036854775807, 0, vrepldb, binlogdatapb.VReplicationWorkflowType_MoveTables, 0, false)
4245+
query := binlogplayer.CreateVReplication("test", bls, pos, 9223372036854775807, 9223372036854775807, 0, vrepldb, workflowType, 0, false)
42394246
qr, err := playerEngine.Exec(query)
42404247
require.NoError(t, err)
42414248
expectDBClientQueries(t, qh.Expect(

0 commit comments

Comments
 (0)