github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/redo/reader/reader_test.go (about) 1 // Copyright 2021 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package reader 15 16 import ( 17 "context" 18 "fmt" 19 "net/url" 20 "os" 21 "path/filepath" 22 "testing" 23 "time" 24 25 "github.com/google/uuid" 26 "github.com/pingcap/tiflow/cdc/model" 27 "github.com/pingcap/tiflow/cdc/model/codec" 28 "github.com/pingcap/tiflow/cdc/redo/common" 29 "github.com/pingcap/tiflow/cdc/redo/writer" 30 "github.com/pingcap/tiflow/cdc/redo/writer/file" 31 "github.com/pingcap/tiflow/pkg/redo" 32 "github.com/stretchr/testify/require" 33 "golang.org/x/sync/errgroup" 34 ) 35 36 func genLogFile( 37 ctx context.Context, t *testing.T, 38 dir string, logType string, 39 minCommitTs, maxCommitTs uint64, 40 ) { 41 cfg := &writer.LogWriterConfig{ 42 MaxLogSizeInBytes: 100000, 43 Dir: dir, 44 } 45 fileName := fmt.Sprintf(redo.RedoLogFileFormatV2, "capture", "default", 46 "changefeed", logType, maxCommitTs, uuid.NewString(), redo.LogEXT) 47 w, err := file.NewFileWriter(ctx, cfg, writer.WithLogFileName(func() string { 48 return fileName 49 })) 50 require.Nil(t, err) 51 if logType == redo.RedoRowLogFileType { 52 // generate unsorted logs 53 for ts := maxCommitTs; ts >= minCommitTs; ts-- { 54 event := &model.RowChangedEvent{ 55 CommitTs: ts, 56 TableInfo: &model.TableInfo{ 57 TableName: model.TableName{ 58 Schema: "test", 59 Table: "t", 60 }, 61 }, 62 } 63 log := event.ToRedoLog() 64 rawData, err := codec.MarshalRedoLog(log, nil) 65 require.Nil(t, err) 66 _, err = w.Write(rawData) 67 require.Nil(t, err) 68 } 69 } else if logType == redo.RedoDDLLogFileType { 70 event := &model.DDLEvent{ 71 CommitTs: maxCommitTs, 72 TableInfo: &model.TableInfo{}, 73 } 74 log := event.ToRedoLog() 75 rawData, err := codec.MarshalRedoLog(log, nil) 76 require.Nil(t, err) 77 _, err = w.Write(rawData) 78 require.Nil(t, err) 79 } 80 err = w.Close() 81 require.Nil(t, err) 82 } 83 84 func TestReadLogs(t *testing.T) { 85 t.Parallel() 86 87 dir := t.TempDir() 88 ctx, cancel := context.WithCancel(context.Background()) 89 90 meta := &common.LogMeta{ 91 CheckpointTs: 11, 92 ResolvedTs: 100, 93 } 94 for _, logType := range []string{redo.RedoRowLogFileType, redo.RedoDDLLogFileType} { 95 genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs) 96 genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs) 97 genLogFile(ctx, t, dir, logType, 12, 12) 98 genLogFile(ctx, t, dir, logType, meta.ResolvedTs, meta.ResolvedTs) 99 } 100 expectedRows := []uint64{12, meta.ResolvedTs} 101 expectedDDLs := []uint64{meta.CheckpointTs, meta.CheckpointTs, 12, meta.ResolvedTs} 102 103 uri, err := url.Parse(fmt.Sprintf("file://%s", dir)) 104 require.NoError(t, err) 105 r := &LogReader{ 106 cfg: &LogReaderConfig{ 107 Dir: t.TempDir(), 108 URI: *uri, 109 UseExternalStorage: true, 110 }, 111 meta: meta, 112 rowCh: make(chan *model.RowChangedEventInRedoLog, defaultReaderChanSize), 113 ddlCh: make(chan *model.DDLEvent, defaultReaderChanSize), 114 } 115 eg, egCtx := errgroup.WithContext(ctx) 116 eg.Go(func() error { 117 return r.Run(egCtx) 118 }) 119 120 for _, ts := range expectedRows { 121 row, err := r.ReadNextRow(egCtx) 122 require.NoError(t, err) 123 require.Equal(t, ts, row.CommitTs) 124 } 125 for _, ts := range expectedDDLs { 126 ddl, err := r.ReadNextDDL(egCtx) 127 require.NoError(t, err) 128 require.Equal(t, ts, ddl.CommitTs) 129 } 130 131 cancel() 132 require.ErrorIs(t, eg.Wait(), nil) 133 } 134 135 func TestLogReaderClose(t *testing.T) { 136 t.Parallel() 137 138 dir := t.TempDir() 139 ctx, cancel := context.WithCancel(context.Background()) 140 141 meta := &common.LogMeta{ 142 CheckpointTs: 11, 143 ResolvedTs: 100, 144 } 145 for _, logType := range []string{redo.RedoRowLogFileType, redo.RedoDDLLogFileType} { 146 genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs) 147 genLogFile(ctx, t, dir, logType, meta.CheckpointTs, meta.CheckpointTs) 148 genLogFile(ctx, t, dir, logType, 12, 12) 149 genLogFile(ctx, t, dir, logType, meta.ResolvedTs, meta.CheckpointTs) 150 } 151 152 uri, err := url.Parse(fmt.Sprintf("file://%s", dir)) 153 require.NoError(t, err) 154 r := &LogReader{ 155 cfg: &LogReaderConfig{ 156 Dir: t.TempDir(), 157 URI: *uri, 158 UseExternalStorage: true, 159 }, 160 meta: meta, 161 rowCh: make(chan *model.RowChangedEventInRedoLog, 1), 162 ddlCh: make(chan *model.DDLEvent, 1), 163 } 164 eg, egCtx := errgroup.WithContext(ctx) 165 eg.Go(func() error { 166 return r.Run(egCtx) 167 }) 168 169 time.Sleep(2 * time.Second) 170 cancel() 171 require.ErrorIs(t, eg.Wait(), context.Canceled) 172 } 173 174 func TestNewLogReaderAndReadMeta(t *testing.T) { 175 t.Parallel() 176 177 dir := t.TempDir() 178 genMetaFile(t, dir, &common.LogMeta{ 179 CheckpointTs: 11, 180 ResolvedTs: 22, 181 }) 182 genMetaFile(t, dir, &common.LogMeta{ 183 CheckpointTs: 12, 184 ResolvedTs: 21, 185 }) 186 187 tests := []struct { 188 name string 189 dir string 190 wantCheckpointTs, wantResolvedTs uint64 191 wantErr string 192 }{ 193 { 194 name: "happy", 195 dir: dir, 196 wantCheckpointTs: 12, 197 wantResolvedTs: 22, 198 }, 199 { 200 name: "no meta file", 201 dir: t.TempDir(), 202 wantErr: ".*no redo meta file found in dir*.", 203 }, 204 { 205 name: "wrong dir", 206 dir: "xxx", 207 wantErr: ".*fail to open storage for redo log*.", 208 }, 209 { 210 name: "context cancel", 211 dir: dir, 212 wantCheckpointTs: 12, 213 wantResolvedTs: 22, 214 wantErr: context.Canceled.Error(), 215 }, 216 } 217 for _, tt := range tests { 218 ctx := context.Background() 219 if tt.name == "context cancel" { 220 ctx1, cancel := context.WithCancel(context.Background()) 221 cancel() 222 ctx = ctx1 223 } 224 uriStr := fmt.Sprintf("file://%s", tt.dir) 225 uri, err := url.Parse(uriStr) 226 require.Nil(t, err) 227 l, err := newLogReader(ctx, &LogReaderConfig{ 228 Dir: t.TempDir(), 229 URI: *uri, 230 UseExternalStorage: redo.IsExternalStorage(uri.Scheme), 231 }) 232 if tt.wantErr != "" { 233 require.Regexp(t, tt.wantErr, err, tt.name) 234 } else { 235 require.Nil(t, err, tt.name) 236 cts, rts, err := l.ReadMeta(ctx) 237 require.Nil(t, err, tt.name) 238 require.Equal(t, tt.wantCheckpointTs, cts, tt.name) 239 require.Equal(t, tt.wantResolvedTs, rts, tt.name) 240 } 241 } 242 } 243 244 func genMetaFile(t *testing.T, dir string, meta *common.LogMeta) { 245 fileName := fmt.Sprintf(redo.RedoMetaFileFormat, "capture", "default", 246 "changefeed", redo.RedoMetaFileType, uuid.NewString(), redo.MetaEXT) 247 path := filepath.Join(dir, fileName) 248 f, err := os.Create(path) 249 require.Nil(t, err) 250 data, err := meta.MarshalMsg(nil) 251 require.Nil(t, err) 252 _, err = f.Write(data) 253 require.Nil(t, err) 254 } 255 256 func TestLogHeapLess(t *testing.T) { 257 tests := []struct { 258 name string 259 h logHeap 260 i int 261 j int 262 expect bool 263 }{ 264 { 265 name: "Delete before Update", 266 h: logHeap{ 267 { 268 data: &model.RedoLog{ 269 Type: model.RedoLogTypeRow, 270 RedoRow: model.RedoRowChangedEvent{ 271 Row: &model.RowChangedEventInRedoLog{ 272 CommitTs: 100, 273 Table: &model.TableName{ 274 Schema: "test", 275 Table: "table", 276 TableID: 1, 277 IsPartition: false, 278 }, 279 PreColumns: []*model.Column{ 280 { 281 Name: "col-1", 282 Value: 1, 283 }, { 284 Name: "col-2", 285 Value: 2, 286 }, 287 }, 288 }, 289 }, 290 }, 291 }, 292 { 293 data: &model.RedoLog{ 294 Type: model.RedoLogTypeRow, 295 RedoRow: model.RedoRowChangedEvent{ 296 Row: &model.RowChangedEventInRedoLog{ 297 CommitTs: 100, 298 Table: &model.TableName{ 299 Schema: "test", 300 Table: "table", 301 TableID: 1, 302 IsPartition: false, 303 }, 304 PreColumns: []*model.Column{ 305 { 306 Name: "col-1", 307 Value: 1, 308 }, { 309 Name: "col-2", 310 Value: 2, 311 }, 312 }, 313 Columns: []*model.Column{ 314 { 315 Name: "col-1", 316 Value: 1, 317 }, { 318 Name: "col-2", 319 Value: 3, 320 }, 321 }, 322 }, 323 }, 324 }, 325 }, 326 }, 327 i: 0, 328 j: 1, 329 expect: true, 330 }, 331 { 332 name: "Update before Insert", 333 h: logHeap{ 334 { 335 data: &model.RedoLog{ 336 Type: model.RedoLogTypeRow, 337 RedoRow: model.RedoRowChangedEvent{ 338 Row: &model.RowChangedEventInRedoLog{ 339 CommitTs: 100, 340 Table: &model.TableName{ 341 Schema: "test", 342 Table: "table", 343 TableID: 1, 344 IsPartition: false, 345 }, 346 PreColumns: []*model.Column{ 347 { 348 Name: "col-1", 349 Value: 1, 350 }, { 351 Name: "col-2", 352 Value: 2, 353 }, 354 }, 355 Columns: []*model.Column{ 356 { 357 Name: "col-1", 358 Value: 1, 359 }, { 360 Name: "col-2", 361 Value: 3, 362 }, 363 }, 364 }, 365 }, 366 }, 367 }, 368 { 369 data: &model.RedoLog{ 370 Type: model.RedoLogTypeRow, 371 RedoRow: model.RedoRowChangedEvent{ 372 Row: &model.RowChangedEventInRedoLog{ 373 CommitTs: 100, 374 Table: &model.TableName{ 375 Schema: "test", 376 Table: "table", 377 TableID: 1, 378 IsPartition: false, 379 }, 380 Columns: []*model.Column{ 381 { 382 Name: "col-1", 383 Value: 1, 384 }, { 385 Name: "col-2", 386 Value: 1, 387 }, 388 }, 389 }, 390 }, 391 }, 392 }, 393 }, 394 i: 0, 395 j: 1, 396 expect: true, 397 }, 398 { 399 name: "Update before Delete", 400 h: logHeap{ 401 { 402 data: &model.RedoLog{ 403 Type: model.RedoLogTypeRow, 404 RedoRow: model.RedoRowChangedEvent{ 405 Row: &model.RowChangedEventInRedoLog{ 406 CommitTs: 100, 407 Table: &model.TableName{ 408 Schema: "test", 409 Table: "table", 410 TableID: 1, 411 IsPartition: false, 412 }, 413 PreColumns: []*model.Column{ 414 { 415 Name: "col-1", 416 Value: 1, 417 }, { 418 Name: "col-2", 419 Value: 2, 420 }, 421 }, 422 Columns: []*model.Column{ 423 { 424 Name: "col-1", 425 Value: 1, 426 }, { 427 Name: "col-2", 428 Value: 3, 429 }, 430 }, 431 }, 432 }, 433 }, 434 }, 435 { 436 data: &model.RedoLog{ 437 Type: model.RedoLogTypeRow, 438 RedoRow: model.RedoRowChangedEvent{ 439 Row: &model.RowChangedEventInRedoLog{ 440 CommitTs: 100, 441 Table: &model.TableName{ 442 Schema: "test", 443 Table: "table", 444 TableID: 1, 445 IsPartition: false, 446 }, 447 PreColumns: []*model.Column{ 448 { 449 Name: "col-1", 450 Value: 1, 451 }, { 452 Name: "col-2", 453 Value: 1, 454 }, 455 }, 456 }, 457 }, 458 }, 459 }, 460 }, 461 i: 0, 462 j: 1, 463 expect: false, 464 }, 465 { 466 name: "Update before Update", 467 h: logHeap{ 468 { 469 data: &model.RedoLog{ 470 Type: model.RedoLogTypeRow, 471 RedoRow: model.RedoRowChangedEvent{ 472 Row: &model.RowChangedEventInRedoLog{ 473 CommitTs: 100, 474 Table: &model.TableName{ 475 Schema: "test", 476 Table: "table", 477 TableID: 1, 478 IsPartition: false, 479 }, 480 PreColumns: []*model.Column{ 481 { 482 Name: "col-1", 483 Value: 1, 484 }, { 485 Name: "col-2", 486 Value: 2, 487 }, 488 }, 489 Columns: []*model.Column{ 490 { 491 Name: "col-1", 492 Value: 1, 493 }, { 494 Name: "col-2", 495 Value: 3, 496 }, 497 }, 498 }, 499 }, 500 }, 501 }, 502 { 503 data: &model.RedoLog{ 504 Type: model.RedoLogTypeRow, 505 RedoRow: model.RedoRowChangedEvent{ 506 Row: &model.RowChangedEventInRedoLog{ 507 CommitTs: 100, 508 Table: &model.TableName{ 509 Schema: "test", 510 Table: "table", 511 TableID: 1, 512 IsPartition: false, 513 }, 514 PreColumns: []*model.Column{ 515 { 516 Name: "col-1", 517 Value: 1, 518 }, { 519 Name: "col-2", 520 Value: 1, 521 }, 522 }, 523 Columns: []*model.Column{ 524 { 525 Name: "col-1", 526 Value: 1, 527 }, { 528 Name: "col-2", 529 Value: 4, 530 }, 531 }, 532 }, 533 }, 534 }, 535 }, 536 }, 537 i: 0, 538 j: 1, 539 expect: true, 540 }, 541 { 542 name: "Delete before Insert", 543 h: logHeap{ 544 { 545 data: &model.RedoLog{ 546 Type: model.RedoLogTypeRow, 547 RedoRow: model.RedoRowChangedEvent{ 548 Row: &model.RowChangedEventInRedoLog{ 549 CommitTs: 100, 550 Table: &model.TableName{ 551 Schema: "test", 552 Table: "table", 553 TableID: 1, 554 IsPartition: false, 555 }, 556 PreColumns: []*model.Column{ 557 { 558 Name: "col-1", 559 Value: 1, 560 }, { 561 Name: "col-2", 562 Value: 1, 563 }, 564 }, 565 }, 566 }, 567 }, 568 }, 569 { 570 data: &model.RedoLog{ 571 Type: model.RedoLogTypeRow, 572 RedoRow: model.RedoRowChangedEvent{ 573 Row: &model.RowChangedEventInRedoLog{ 574 CommitTs: 100, 575 Table: &model.TableName{ 576 Schema: "test", 577 Table: "table", 578 TableID: 1, 579 IsPartition: false, 580 }, 581 Columns: []*model.Column{ 582 { 583 Name: "col-1", 584 Value: 1, 585 }, { 586 Name: "col-2", 587 Value: 3, 588 }, 589 }, 590 }, 591 }, 592 }, 593 }, 594 }, 595 i: 0, 596 j: 1, 597 expect: true, 598 }, 599 { 600 name: "Same type of operations, different commit ts", 601 h: logHeap{ 602 { 603 data: &model.RedoLog{ 604 Type: model.RedoLogTypeRow, 605 RedoRow: model.RedoRowChangedEvent{ 606 Row: &model.RowChangedEventInRedoLog{ 607 CommitTs: 100, 608 Table: &model.TableName{ 609 Schema: "test", 610 Table: "table", 611 TableID: 1, 612 IsPartition: false, 613 }, 614 }, 615 }, 616 }, 617 }, 618 { 619 data: &model.RedoLog{ 620 Type: model.RedoLogTypeRow, 621 RedoRow: model.RedoRowChangedEvent{ 622 Row: &model.RowChangedEventInRedoLog{ 623 CommitTs: 200, 624 Table: &model.TableName{ 625 Schema: "test", 626 Table: "table", 627 TableID: 1, 628 IsPartition: false, 629 }, 630 }, 631 }, 632 }, 633 }, 634 }, 635 i: 0, 636 j: 1, 637 expect: true, 638 }, 639 { 640 name: "Same type of operations, same commit ts, different startTs", 641 h: logHeap{ 642 { 643 data: &model.RedoLog{ 644 Type: model.RedoLogTypeRow, 645 RedoRow: model.RedoRowChangedEvent{ 646 Row: &model.RowChangedEventInRedoLog{ 647 CommitTs: 100, 648 StartTs: 80, 649 Table: &model.TableName{ 650 Schema: "test", 651 Table: "table", 652 TableID: 1, 653 IsPartition: false, 654 }, 655 }, 656 }, 657 }, 658 }, 659 { 660 data: &model.RedoLog{ 661 Type: model.RedoLogTypeRow, 662 RedoRow: model.RedoRowChangedEvent{ 663 Row: &model.RowChangedEventInRedoLog{ 664 CommitTs: 100, 665 StartTs: 90, 666 Table: &model.TableName{ 667 Schema: "test", 668 Table: "table", 669 TableID: 1, 670 IsPartition: false, 671 }, 672 }, 673 }, 674 }, 675 }, 676 }, 677 i: 0, 678 j: 1, 679 expect: true, 680 }, 681 { 682 name: "Same type of operations, same commit ts", 683 h: logHeap{ 684 { 685 data: &model.RedoLog{ 686 Type: model.RedoLogTypeRow, 687 RedoRow: model.RedoRowChangedEvent{ 688 Row: &model.RowChangedEventInRedoLog{ 689 CommitTs: 100, 690 Table: &model.TableName{ 691 Schema: "test", 692 Table: "table", 693 TableID: 1, 694 IsPartition: false, 695 }, 696 PreColumns: []*model.Column{ 697 { 698 Name: "col-1", 699 Value: 1, 700 }, { 701 Name: "col-2", 702 Value: 2, 703 }, 704 }, 705 Columns: []*model.Column{ 706 { 707 Name: "col-1", 708 Value: 1, 709 }, { 710 Name: "col-2", 711 Value: 3, 712 }, 713 }, 714 }, 715 }, 716 }, 717 }, 718 { 719 data: &model.RedoLog{ 720 Type: model.RedoLogTypeRow, 721 RedoRow: model.RedoRowChangedEvent{ 722 Row: &model.RowChangedEventInRedoLog{ 723 CommitTs: 100, 724 Table: &model.TableName{ 725 Schema: "test", 726 Table: "table", 727 TableID: 1, 728 IsPartition: false, 729 }, 730 PreColumns: []*model.Column{ 731 { 732 Name: "col-1", 733 Value: 1, 734 }, { 735 Name: "col-2", 736 Value: 1, 737 }, 738 }, 739 Columns: []*model.Column{ 740 { 741 Name: "col-1", 742 Value: 1, 743 }, { 744 Name: "col-2", 745 Value: 3, 746 }, 747 }, 748 }, 749 }, 750 }, 751 }, 752 }, 753 i: 0, 754 j: 1, 755 expect: true, 756 }, 757 } 758 759 for _, tt := range tests { 760 t.Run(tt.name, func(t *testing.T) { 761 if got := tt.h.Less(tt.i, tt.j); got != tt.expect { 762 t.Errorf("logHeap.Less() = %v, want %v", got, tt.expect) 763 } 764 }) 765 } 766 }