github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/relay/local_reader_test.go (about) 1 // Copyright 2019 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 relay 15 16 import ( 17 "bytes" 18 "context" 19 "fmt" 20 "io" 21 "math/rand" 22 "os" 23 "path" 24 "path/filepath" 25 "strconv" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/BurntSushi/toml" 31 gmysql "github.com/go-mysql-org/go-mysql/mysql" 32 "github.com/go-mysql-org/go-mysql/replication" 33 "github.com/google/uuid" 34 . "github.com/pingcap/check" 35 "github.com/pingcap/errors" 36 "github.com/pingcap/failpoint" 37 "github.com/pingcap/tiflow/dm/pkg/binlog/event" 38 "github.com/pingcap/tiflow/dm/pkg/binlog/reader" 39 "github.com/pingcap/tiflow/dm/pkg/gtid" 40 "github.com/pingcap/tiflow/dm/pkg/log" 41 "github.com/pingcap/tiflow/dm/pkg/terror" 42 "github.com/pingcap/tiflow/dm/pkg/utils" 43 ) 44 45 var parseFileTimeout = 10 * time.Second 46 47 var _ = Suite(&testReaderSuite{}) 48 49 type testReaderSuite struct { 50 lastPos uint32 51 lastGTID gmysql.GTIDSet 52 } 53 54 func (t *testReaderSuite) SetUpSuite(c *C) { 55 var err error 56 t.lastPos = 0 57 t.lastGTID, err = gtid.ParserGTID(gmysql.MySQLFlavor, "ba8f633f-1f15-11eb-b1c7-0242ac110002:1") 58 c.Assert(err, IsNil) 59 c.Assert(failpoint.Enable("github.com/pingcap/tiflow/dm/relay/SetHeartbeatInterval", "return(10000)"), IsNil) 60 } 61 62 func (t *testReaderSuite) TearDownSuite(c *C) { 63 c.Assert(failpoint.Disable("github.com/pingcap/tiflow/dm/relay/SetHeartbeatInterval"), IsNil) 64 } 65 66 func newBinlogReaderForTest(logger log.Logger, cfg *BinlogReaderConfig, notify bool, uuid string) *BinlogReader { 67 relay := NewRealRelay(&Config{Flavor: gmysql.MySQLFlavor}) 68 r := newBinlogReader(logger, cfg, relay) 69 if notify { 70 r.notifyCh <- struct{}{} 71 } 72 r.currentSubDir = uuid 73 return r 74 } 75 76 func (t *testReaderSuite) setActiveRelayLog(r Process, uuid, filename string, offset int64) { 77 relay := r.(*Relay) 78 writer := relay.writer.(*FileWriter) 79 writer.out.uuid.Store(uuid) 80 writer.out.filename.Store(filename) 81 writer.out.offset.Store(offset) 82 } 83 84 func (t *testReaderSuite) createBinlogFileParseState(c *C, relayLogDir, relayLogFile string, offset int64, possibleLast bool) *binlogFileParseState { 85 fullPath := filepath.Join(relayLogDir, relayLogFile) 86 f, err := os.Open(fullPath) 87 c.Assert(err, IsNil) 88 89 return &binlogFileParseState{ 90 possibleLast: possibleLast, 91 fullPath: fullPath, 92 relayLogFile: relayLogFile, 93 relayLogDir: relayLogDir, 94 f: f, 95 latestPos: offset, 96 skipGTID: false, 97 } 98 } 99 100 func (t *testReaderSuite) TestparseFileAsPossibleFileNotExist(c *C) { 101 var ( 102 baseDir = c.MkDir() 103 currentUUID = "b60868af-5a6f-11e9-9ea3-0242ac160006.000001" 104 filename = "test-mysql-bin.000001" 105 relayDir = path.Join(baseDir, currentUUID) 106 ) 107 cfg := &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 108 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 109 needSwitch, lastestPos, err := r.parseFileAsPossible(context.Background(), nil, filename, 4, relayDir, true, false) 110 c.Assert(needSwitch, IsFalse) 111 c.Assert(lastestPos, Equals, int64(0)) 112 c.Assert(err, ErrorMatches, ".*no such file or directory.*") 113 } 114 115 func (t *testReaderSuite) TestParseFileBase(c *C) { 116 var ( 117 filename = "test-mysql-bin.000001" 118 baseDir = c.MkDir() 119 possibleLast = false 120 baseEvents, _, _ = t.genBinlogEvents(c, t.lastPos, t.lastGTID) 121 s = newLocalStreamer() 122 ) 123 ctx, cancel := context.WithCancel(context.Background()) 124 defer cancel() 125 126 // change to valid currentSubDir 127 currentUUID := "b60868af-5a6f-11e9-9ea3-0242ac160006.000001" 128 relayDir := filepath.Join(baseDir, currentUUID) 129 fullPath := filepath.Join(relayDir, filename) 130 err1 := os.MkdirAll(relayDir, 0o700) 131 c.Assert(err1, IsNil) 132 f, err1 := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0o600) 133 c.Assert(err1, IsNil) 134 defer f.Close() 135 136 // empty relay log file, got EOF when reading format description event separately and possibleLast = false 137 { 138 testCtx, testCancel := context.WithTimeout(ctx, 500*time.Millisecond) 139 defer testCancel() 140 cfg := &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 141 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 142 t.setActiveRelayLog(r.relay, currentUUID, filename, 0) 143 state := t.createBinlogFileParseState(c, relayDir, filename, 100, possibleLast) 144 needSwitch, needReParse, err := r.parseFile(testCtx, s, true, state) 145 c.Assert(errors.Cause(err), Equals, io.EOF) 146 c.Assert(needSwitch, IsFalse) 147 c.Assert(needReParse, IsFalse) 148 c.Assert(state.latestPos, Equals, int64(100)) 149 c.Assert(state.formatDescEventRead, IsFalse) 150 c.Assert(state.skipGTID, Equals, false) 151 } 152 153 // write some events to binlog file 154 _, err1 = f.Write(replication.BinLogFileHeader) 155 c.Assert(err1, IsNil) 156 for _, ev := range baseEvents { 157 _, err1 = f.Write(ev.RawData) 158 c.Assert(err1, IsNil) 159 } 160 fileSize, _ := f.Seek(0, io.SeekCurrent) 161 162 t.purgeStreamer(c, s) 163 164 // base test with only one valid binlog file 165 { 166 testCtx, testCancel := context.WithTimeout(ctx, 500*time.Millisecond) 167 defer testCancel() 168 cfg := &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 169 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 170 t.setActiveRelayLog(r.relay, currentUUID, filename, fileSize) 171 state := t.createBinlogFileParseState(c, relayDir, filename, 4, possibleLast) 172 needSwitch, needReParse, err := r.parseFile(testCtx, s, true, state) 173 c.Assert(err, IsNil) 174 c.Assert(needSwitch, IsFalse) 175 c.Assert(needReParse, IsFalse) 176 c.Assert(state.latestPos, Equals, int64(baseEvents[len(baseEvents)-1].Header.LogPos)) 177 c.Assert(state.formatDescEventRead, IsTrue) 178 c.Assert(state.skipGTID, IsFalse) 179 180 // try get events back, firstParse should have fake RotateEvent 181 var fakeRotateEventCount int 182 i := 0 183 for { 184 ev, err2 := s.GetEvent(ctx) 185 c.Assert(err2, IsNil) 186 if ev.Header.Timestamp == 0 || ev.Header.LogPos == 0 { 187 if ev.Header.EventType == replication.ROTATE_EVENT { 188 fakeRotateEventCount++ 189 } 190 continue // ignore fake event 191 } 192 c.Assert(ev, DeepEquals, baseEvents[i]) 193 i++ 194 if i >= len(baseEvents) { 195 break 196 } 197 } 198 c.Assert(fakeRotateEventCount, Equals, 1) 199 t.verifyNoEventsInStreamer(c, s) 200 } 201 202 // try get events back, since firstParse=false, should have no fake RotateEvent 203 { 204 testCtx, testCancel := context.WithTimeout(ctx, 500*time.Millisecond) 205 defer testCancel() 206 cfg := &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 207 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 208 t.setActiveRelayLog(r.relay, currentUUID, filename, fileSize) 209 state := t.createBinlogFileParseState(c, relayDir, filename, 4, possibleLast) 210 needSwitch, needReParse, err := r.parseFile(testCtx, s, false, state) 211 c.Assert(err, IsNil) 212 c.Assert(needSwitch, IsFalse) 213 c.Assert(needReParse, IsFalse) 214 c.Assert(state.latestPos, Equals, int64(baseEvents[len(baseEvents)-1].Header.LogPos)) 215 c.Assert(state.formatDescEventRead, IsTrue) 216 c.Assert(state.skipGTID, Equals, false) 217 fakeRotateEventCount := 0 218 i := 0 219 for { 220 ev, err2 := s.GetEvent(ctx) 221 c.Assert(err2, IsNil) 222 if ev.Header.Timestamp == 0 || ev.Header.LogPos == 0 { 223 if ev.Header.EventType == replication.ROTATE_EVENT { 224 fakeRotateEventCount++ 225 } 226 continue // ignore fake event 227 } 228 c.Assert(ev, DeepEquals, baseEvents[i]) 229 i++ 230 if i >= len(baseEvents) { 231 break 232 } 233 } 234 c.Assert(fakeRotateEventCount, Equals, 0) 235 t.verifyNoEventsInStreamer(c, s) 236 } 237 238 // generate another non-fake RotateEvent 239 rotateEv, err := event.GenRotateEvent(baseEvents[0].Header, uint32(fileSize), []byte("mysql-bin.888888"), 4) 240 c.Assert(err, IsNil) 241 _, err = f.Write(rotateEv.RawData) 242 c.Assert(err, IsNil) 243 fileSize, _ = f.Seek(0, io.SeekCurrent) 244 245 // latest is still the end_log_pos of the last event, not the next relay file log file's position 246 { 247 testCtx, testCancel := context.WithTimeout(ctx, 500*time.Millisecond) 248 defer testCancel() 249 cfg := &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 250 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 251 t.setActiveRelayLog(r.relay, currentUUID, filename, fileSize) 252 state := t.createBinlogFileParseState(c, relayDir, filename, 4, possibleLast) 253 needSwitch, needReParse, err := r.parseFile(testCtx, s, true, state) 254 c.Assert(err, IsNil) 255 c.Assert(needSwitch, IsFalse) 256 c.Assert(needReParse, IsFalse) 257 c.Assert(state.latestPos, Equals, int64(rotateEv.Header.LogPos)) 258 c.Assert(state.formatDescEventRead, IsTrue) 259 c.Assert(state.skipGTID, Equals, false) 260 t.purgeStreamer(c, s) 261 } 262 263 // parse from offset > 4 264 { 265 testCtx, testCancel := context.WithTimeout(ctx, 500*time.Millisecond) 266 defer testCancel() 267 cfg := &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 268 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 269 t.setActiveRelayLog(r.relay, currentUUID, filename, fileSize) 270 offset := int64(rotateEv.Header.LogPos - rotateEv.Header.EventSize) 271 state := t.createBinlogFileParseState(c, relayDir, filename, offset, possibleLast) 272 needSwitch, needReParse, err := r.parseFile(testCtx, s, false, state) 273 c.Assert(err, IsNil) 274 c.Assert(needSwitch, IsFalse) 275 c.Assert(needReParse, IsFalse) 276 c.Assert(state.latestPos, Equals, int64(rotateEv.Header.LogPos)) 277 c.Assert(state.formatDescEventRead, IsTrue) 278 c.Assert(state.skipGTID, Equals, false) 279 280 // should only get a RotateEvent 281 i := 0 282 for { 283 ev, err2 := s.GetEvent(ctx) 284 c.Assert(err2, IsNil) 285 switch ev.Header.EventType { 286 case replication.ROTATE_EVENT: 287 c.Assert(ev.RawData, DeepEquals, rotateEv.RawData) 288 i++ 289 default: 290 c.Fatalf("got unexpected event %+v", ev.Header) 291 } 292 if i >= 1 { 293 break 294 } 295 } 296 t.verifyNoEventsInStreamer(c, s) 297 } 298 } 299 300 func (t *testReaderSuite) TestParseFileRelayNeedSwitchSubDir(c *C) { 301 var ( 302 filename = "test-mysql-bin.000001" 303 nextFilename = "test-mysql-bin.666888" 304 notUsedGTIDSetStr = t.lastGTID.String() 305 baseDir = c.MkDir() 306 offset int64 307 possibleLast = true 308 currentUUID = "b60868af-5a6f-11e9-9ea3-0242ac160006.000001" 309 switchedUUID = "b60868af-5a6f-11e9-9ea3-0242ac160007.000002" 310 relayDir = filepath.Join(baseDir, currentUUID) 311 nextRelayDir = filepath.Join(baseDir, switchedUUID) 312 fullPath = filepath.Join(relayDir, filename) 313 nextFullPath = filepath.Join(nextRelayDir, nextFilename) 314 s = newLocalStreamer() 315 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 316 r = newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 317 ) 318 319 // create the current relay log file and meta 320 err := os.MkdirAll(relayDir, 0o700) 321 c.Assert(err, IsNil) 322 f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0o600) 323 c.Assert(err, IsNil) 324 defer f.Close() 325 _, err = f.Write(replication.BinLogFileHeader) 326 offset = 4 327 c.Assert(err, IsNil) 328 t.createMetaFile(c, relayDir, filename, uint32(offset), notUsedGTIDSetStr) 329 330 r.subDirs = []string{currentUUID, switchedUUID} 331 t.writeUUIDs(c, baseDir, r.subDirs) 332 err = os.MkdirAll(nextRelayDir, 0o700) 333 c.Assert(err, IsNil) 334 err = os.WriteFile(nextFullPath, replication.BinLogFileHeader, 0o600) 335 c.Assert(err, IsNil) 336 337 // has relay log file in next sub directory, need to switch 338 ctx2, cancel2 := context.WithTimeout(context.Background(), parseFileTimeout) 339 defer cancel2() 340 t.createMetaFile(c, nextRelayDir, filename, uint32(offset), notUsedGTIDSetStr) 341 state := t.createBinlogFileParseState(c, relayDir, filename, offset, possibleLast) 342 state.formatDescEventRead = true 343 t.setActiveRelayLog(r.relay, "next", "next", 4) 344 needSwitch, needReParse, err := r.parseFile(ctx2, s, true, state) 345 c.Assert(err, IsNil) 346 c.Assert(needSwitch, IsTrue) 347 c.Assert(needReParse, IsFalse) 348 c.Assert(state.latestPos, Equals, int64(4)) 349 c.Assert(state.formatDescEventRead, IsTrue) 350 c.Assert(state.skipGTID, Equals, false) 351 t.purgeStreamer(c, s) 352 353 // NOTE: if we want to test the returned `needReParse` of `needSwitchSubDir`, 354 // then we need to mock `fileSizeUpdated` or inject some delay or delay. 355 } 356 357 func (t *testReaderSuite) TestParseFileRelayWithIgnorableError(c *C) { 358 var ( 359 filename = "test-mysql-bin.000001" 360 notUsedGTIDSetStr = t.lastGTID.String() 361 baseDir = c.MkDir() 362 possibleLast = true 363 baseEvents, _, _ = t.genBinlogEvents(c, t.lastPos, t.lastGTID) 364 currentUUID = "b60868af-5a6f-11e9-9ea3-0242ac160006.000001" 365 relayDir = filepath.Join(baseDir, currentUUID) 366 fullPath = filepath.Join(relayDir, filename) 367 s = newLocalStreamer() 368 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 369 ) 370 371 // create the current relay log file and write some events 372 err := os.MkdirAll(relayDir, 0o700) 373 c.Assert(err, IsNil) 374 f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0o600) 375 c.Assert(err, IsNil) 376 defer f.Close() 377 t.createMetaFile(c, relayDir, filename, 0, notUsedGTIDSetStr) 378 379 _, err = f.Write(replication.BinLogFileHeader) 380 c.Assert(err, IsNil) 381 _, err = f.Write(baseEvents[0].RawData[:replication.EventHeaderSize]) 382 c.Assert(err, IsNil) 383 384 // meet io.EOF error when read event and ignore it. 385 { 386 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 387 state := t.createBinlogFileParseState(c, relayDir, filename, 4, possibleLast) 388 state.formatDescEventRead = true 389 t.setActiveRelayLog(r.relay, currentUUID, filename, 100) 390 needSwitch, needReParse, err := r.parseFile(context.Background(), s, true, state) 391 c.Assert(err, IsNil) 392 c.Assert(needSwitch, IsFalse) 393 c.Assert(needReParse, IsTrue) 394 c.Assert(state.latestPos, Equals, int64(4)) 395 c.Assert(state.formatDescEventRead, IsTrue) 396 c.Assert(state.skipGTID, Equals, false) 397 } 398 } 399 400 func (t *testReaderSuite) TestUpdateUUIDs(c *C) { 401 var ( 402 baseDir = c.MkDir() 403 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 404 r = newBinlogReaderForTest(log.L(), cfg, true, "") 405 ) 406 c.Assert(r.subDirs, HasLen, 0) 407 408 // index file not exists, got nothing 409 err := r.updateSubDirs() 410 c.Assert(err, IsNil) 411 c.Assert(r.subDirs, HasLen, 0) 412 413 // valid UUIDs in the index file, got them back 414 UUIDs := []string{ 415 "b60868af-5a6f-11e9-9ea3-0242ac160006.000001", 416 "b60868af-5a6f-11e9-9ea3-0242ac160007.000002", 417 } 418 uuidBytes := t.uuidListToBytes(c, UUIDs) 419 err = os.WriteFile(r.indexPath, uuidBytes, 0o600) 420 c.Assert(err, IsNil) 421 422 err = r.updateSubDirs() 423 c.Assert(err, IsNil) 424 c.Assert(r.subDirs, DeepEquals, UUIDs) 425 } 426 427 func (t *testReaderSuite) TestStartSyncByPos(c *C) { 428 var ( 429 filenamePrefix = "test-mysql-bin.00000" 430 notUsedGTIDSetStr = t.lastGTID.String() 431 baseDir = c.MkDir() 432 baseEvents, lastPos, lastGTID = t.genBinlogEvents(c, t.lastPos, t.lastGTID) 433 eventsBuf bytes.Buffer 434 UUIDs = []string{ 435 "b60868af-5a6f-11e9-9ea3-0242ac160006.000001", 436 "b60868af-5a6f-11e9-9ea3-0242ac160007.000002", 437 "b60868af-5a6f-11e9-9ea3-0242ac160008.000003", 438 } 439 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 440 r = newBinlogReaderForTest(log.L(), cfg, false, "") 441 startPos = gmysql.Position{Name: "test-mysql-bin|000001.000001"} // from the first relay log file in the first sub directory 442 ) 443 444 // prepare binlog data 445 _, err := eventsBuf.Write(replication.BinLogFileHeader) 446 c.Assert(err, IsNil) 447 for _, ev := range baseEvents { 448 _, err = eventsBuf.Write(ev.RawData) 449 c.Assert(err, IsNil) 450 } 451 452 // create the index file 453 uuidBytes := t.uuidListToBytes(c, UUIDs) 454 err = os.WriteFile(r.indexPath, uuidBytes, 0o600) 455 c.Assert(err, IsNil) 456 457 // create sub directories 458 for _, uuid := range UUIDs { 459 subDir := filepath.Join(baseDir, uuid) 460 err = os.MkdirAll(subDir, 0o700) 461 c.Assert(err, IsNil) 462 } 463 464 // 1. generate relay log files 465 // 1 for the first sub directory, 2 for the second directory and 3 for the third directory 466 // so, write the same events data into (1+2+3) files. 467 for i := 0; i < 3; i++ { 468 for j := 1; j < i+2; j++ { 469 filename := filepath.Join(baseDir, UUIDs[i], filenamePrefix+strconv.Itoa(j)) 470 var content []byte 471 content = append(content, eventsBuf.Bytes()...) 472 // don't add rotate event for the last file because we'll append more events to it. 473 if !(i == 2 && j == i+1) { 474 rotateEvent, err2 := event.GenRotateEvent(baseEvents[0].Header, lastPos, []byte(filenamePrefix+strconv.Itoa(j+1)), 4) 475 c.Assert(err2, IsNil) 476 content = append(content, rotateEvent.RawData...) 477 } 478 err = os.WriteFile(filename, content, 0o600) 479 c.Assert(err, IsNil) 480 } 481 t.createMetaFile(c, path.Join(baseDir, UUIDs[i]), filenamePrefix+strconv.Itoa(i+1), 482 startPos.Pos, notUsedGTIDSetStr) 483 } 484 485 // start the reader 486 s, err := r.StartSyncByPos(startPos) 487 c.Assert(err, IsNil) 488 489 // get events from the streamer 490 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 491 defer cancel() 492 493 obtainBaseEvents := readNEvents(ctx, c, s, (1+2+3)*(len(baseEvents)+1)-1, false) 494 t.verifyNoEventsInStreamer(c, s) 495 // verify obtain base events 496 for i := 0; i < len(obtainBaseEvents); i += len(baseEvents) { 497 c.Assert(obtainBaseEvents[i:i+len(baseEvents)], DeepEquals, baseEvents) 498 // skip rotate event which is not added in baseEvents 499 i++ 500 } 501 502 // 2. write more events to the last file 503 lastFilename := filepath.Join(baseDir, UUIDs[2], filenamePrefix+strconv.Itoa(3)) 504 extraEvents, _, _ := t.genBinlogEvents(c, lastPos, lastGTID) 505 lastF, err := os.OpenFile(lastFilename, os.O_WRONLY|os.O_APPEND, 0o600) 506 c.Assert(err, IsNil) 507 defer lastF.Close() 508 for _, ev := range extraEvents { 509 _, err = lastF.Write(ev.RawData) 510 c.Assert(err, IsNil) 511 } 512 r.notifyCh <- struct{}{} 513 514 // read extra events back 515 obtainExtraEvents := make([]*replication.BinlogEvent, 0, len(extraEvents)) 516 for { 517 ev, err2 := s.GetEvent(ctx) 518 c.Assert(err2, IsNil) 519 if ev.Header.Timestamp == 0 || ev.Header.LogPos == 0 || ev.Header.EventType == replication.FORMAT_DESCRIPTION_EVENT { 520 continue // ignore fake event and FormatDescriptionEvent, go-mysql may send extra FormatDescriptionEvent 521 } 522 obtainExtraEvents = append(obtainExtraEvents, ev) 523 if len(obtainExtraEvents) == cap(obtainExtraEvents) { 524 break 525 } 526 } 527 t.verifyNoEventsInStreamer(c, s) 528 529 // verify obtain extra events 530 c.Assert(obtainExtraEvents, DeepEquals, extraEvents) 531 532 // 3. create new file in the last directory 533 lastFilename = filepath.Join(baseDir, UUIDs[2], filenamePrefix+strconv.Itoa(4)) 534 err = os.WriteFile(lastFilename, eventsBuf.Bytes(), 0o600) 535 c.Assert(err, IsNil) 536 t.createMetaFile(c, path.Join(baseDir, UUIDs[2]), lastFilename, lastPos, notUsedGTIDSetStr) 537 r.notifyCh <- struct{}{} 538 539 obtainExtraEvents2 := make([]*replication.BinlogEvent, 0, len(baseEvents)-1) 540 for { 541 ev, err2 := s.GetEvent(ctx) 542 c.Assert(err2, IsNil) 543 if ev.Header.Timestamp == 0 || ev.Header.LogPos == 0 || ev.Header.EventType == replication.FORMAT_DESCRIPTION_EVENT { 544 continue // ignore fake event and FormatDescriptionEvent, go-mysql may send extra FormatDescriptionEvent 545 } 546 obtainExtraEvents2 = append(obtainExtraEvents2, ev) 547 if len(obtainExtraEvents2) == cap(obtainExtraEvents2) { 548 break 549 } 550 } 551 t.verifyNoEventsInStreamer(c, s) 552 553 // verify obtain extra events 554 c.Assert(obtainExtraEvents2, DeepEquals, baseEvents[1:]) 555 556 // NOTE: load new UUIDs dynamically not supported yet 557 558 // close the reader 559 r.Close() 560 } 561 562 func readNEvents(ctx context.Context, c *C, s reader.Streamer, l int, tolerateMayDup bool) []*replication.BinlogEvent { 563 var result []*replication.BinlogEvent 564 for { 565 ev, err2 := s.GetEvent(ctx) 566 if tolerateMayDup { 567 if err2 != nil { 568 c.Assert(errors.ErrorEqual(ErrorMaybeDuplicateEvent, err2), IsTrue) 569 continue 570 } 571 } else { 572 c.Assert(err2, IsNil) 573 } 574 if ev.Header.Timestamp == 0 && ev.Header.LogPos == 0 { 575 continue // ignore fake event 576 } 577 result = append(result, ev) 578 // start from the first format description event 579 if len(result) == l { 580 break 581 } 582 } 583 return result 584 } 585 586 func (t *testReaderSuite) TestStartSyncByGTID(c *C) { 587 var ( 588 baseDir = c.MkDir() 589 events []*replication.BinlogEvent 590 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 591 r = newBinlogReaderForTest(log.L(), cfg, false, "") 592 lastPos uint32 593 lastGTID gmysql.GTIDSet 594 previousGset, _ = gtid.ParserGTID(gmysql.MySQLFlavor, "") 595 ) 596 597 type EventResult struct { 598 eventType replication.EventType 599 result string // filename result of getPosByGTID 600 } 601 602 type FileEventResult struct { 603 filename string 604 eventResults []EventResult 605 } 606 607 testCase := []struct { 608 serverUUID string 609 uuid string 610 gtidStr string 611 fileEventResult []FileEventResult 612 }{ 613 { 614 "ba8f633f-1f15-11eb-b1c7-0242ac110002", 615 "ba8f633f-1f15-11eb-b1c7-0242ac110002.000001", 616 "ba8f633f-1f15-11eb-b1c7-0242ac110002:1", 617 []FileEventResult{ 618 { 619 "mysql.000001", 620 []EventResult{ 621 {replication.PREVIOUS_GTIDS_EVENT, ""}, 622 {replication.QUERY_EVENT, "mysql|000001.000001"}, 623 {replication.XID_EVENT, "mysql|000001.000001"}, 624 {replication.QUERY_EVENT, "mysql|000001.000001"}, 625 {replication.XID_EVENT, "mysql|000001.000002"}, // next binlog filename 626 {replication.ROTATE_EVENT, ""}, 627 }, 628 }, 629 { 630 "mysql.000002", 631 []EventResult{ 632 {replication.PREVIOUS_GTIDS_EVENT, ""}, 633 {replication.QUERY_EVENT, "mysql|000001.000002"}, 634 {replication.XID_EVENT, "mysql|000001.000002"}, 635 {replication.QUERY_EVENT, "mysql|000001.000002"}, 636 {replication.XID_EVENT, "mysql|000001.000003"}, // next binlog filename 637 {replication.ROTATE_EVENT, ""}, 638 }, 639 }, 640 { 641 "mysql.000003", 642 []EventResult{ 643 {replication.PREVIOUS_GTIDS_EVENT, ""}, 644 {replication.QUERY_EVENT, "mysql|000001.000003"}, 645 {replication.XID_EVENT, "mysql|000001.000003"}, 646 {replication.QUERY_EVENT, "mysql|000001.000003"}, 647 {replication.XID_EVENT, "mysql|000002.000001"}, // next subdir 648 }, 649 }, 650 }, 651 }, 652 { 653 "bf6227a7-1f15-11eb-9afb-0242ac110004", 654 "bf6227a7-1f15-11eb-9afb-0242ac110004.000002", 655 "bf6227a7-1f15-11eb-9afb-0242ac110004:20", 656 []FileEventResult{ 657 { 658 "mysql.000001", 659 []EventResult{ 660 {replication.PREVIOUS_GTIDS_EVENT, ""}, 661 {replication.QUERY_EVENT, "mysql|000002.000001"}, 662 {replication.XID_EVENT, "mysql|000002.000001"}, 663 {replication.QUERY_EVENT, "mysql|000002.000001"}, 664 {replication.XID_EVENT, "mysql|000002.000002"}, 665 {replication.ROTATE_EVENT, ""}, 666 }, 667 }, { 668 "mysql.000002", 669 []EventResult{ 670 {replication.PREVIOUS_GTIDS_EVENT, ""}, 671 {replication.QUERY_EVENT, "mysql|000002.000002"}, 672 {replication.XID_EVENT, "mysql|000002.000002"}, 673 {replication.QUERY_EVENT, "mysql|000002.000002"}, 674 {replication.XID_EVENT, "mysql|000003.000001"}, 675 {replication.ROTATE_EVENT, ""}, 676 }, 677 }, { 678 "mysql.000003", 679 []EventResult{ 680 {replication.PREVIOUS_GTIDS_EVENT, ""}, 681 }, 682 }, 683 }, 684 }, 685 { 686 "bcbf9d42-1f15-11eb-a41c-0242ac110003", 687 "bcbf9d42-1f15-11eb-a41c-0242ac110003.000003", 688 "bcbf9d42-1f15-11eb-a41c-0242ac110003:30", 689 []FileEventResult{ 690 { 691 "mysql.000001", 692 []EventResult{ 693 {replication.PREVIOUS_GTIDS_EVENT, ""}, 694 {replication.QUERY_EVENT, "mysql|000003.000001"}, 695 {replication.XID_EVENT, "mysql|000003.000001"}, 696 {replication.QUERY_EVENT, "mysql|000003.000001"}, 697 {replication.XID_EVENT, "mysql|000003.000001"}, 698 }, 699 }, 700 }, 701 }, 702 } 703 704 for _, subDir := range testCase { 705 r.subDirs = append(r.subDirs, subDir.uuid) 706 } 707 708 // write index file 709 uuidBytes := t.uuidListToBytes(c, r.subDirs) 710 err := os.WriteFile(r.indexPath, uuidBytes, 0o600) 711 c.Assert(err, IsNil) 712 713 var allEvents []*replication.BinlogEvent 714 var allResults []string 715 var eventsNumOfFirstServer int 716 717 // generate binlog file 718 for i, subDir := range testCase { 719 lastPos = 4 720 lastGTID, err = gtid.ParserGTID(gmysql.MySQLFlavor, subDir.gtidStr) 721 c.Assert(err, IsNil) 722 uuidDir := path.Join(baseDir, subDir.uuid) 723 err = os.MkdirAll(uuidDir, 0o700) 724 c.Assert(err, IsNil) 725 726 for _, fileEventResult := range subDir.fileEventResult { 727 eventTypes := []replication.EventType{} 728 for _, eventResult := range fileEventResult.eventResults { 729 eventTypes = append(eventTypes, eventResult.eventType) 730 if len(eventResult.result) != 0 { 731 allResults = append(allResults, eventResult.result) 732 } 733 } 734 735 // generate events 736 events, lastPos, lastGTID, previousGset = t.genEvents(c, eventTypes, lastPos, lastGTID, previousGset) 737 allEvents = append(allEvents, events...) 738 739 // write binlog file 740 f, err2 := os.OpenFile(path.Join(uuidDir, fileEventResult.filename), os.O_CREATE|os.O_WRONLY, 0o600) 741 c.Assert(err2, IsNil) 742 _, err = f.Write(replication.BinLogFileHeader) 743 c.Assert(err, IsNil) 744 for _, ev := range events { 745 _, err = f.Write(ev.RawData) 746 c.Assert(err, IsNil) 747 } 748 f.Close() 749 t.createMetaFile(c, uuidDir, fileEventResult.filename, lastPos, previousGset.String()) 750 } 751 if i == 0 { 752 eventsNumOfFirstServer = len(allEvents) 753 } 754 } 755 756 startGTID, err := gtid.ParserGTID(gmysql.MySQLFlavor, "") 757 c.Assert(err, IsNil) 758 s, err := r.StartSyncByGTID(startGTID.Clone()) 759 c.Assert(err, IsNil) 760 761 ctx, cancel := context.WithCancel(context.Background()) 762 defer cancel() 763 obtainBaseEvents := readNEvents(ctx, c, s, len(allEvents), true) 764 765 preGset, err := gmysql.ParseGTIDSet(gmysql.MySQLFlavor, "") 766 c.Assert(err, IsNil) 767 768 gtidEventCount := 0 769 for i, ev := range obtainBaseEvents { 770 c.Assert(ev.Header, DeepEquals, allEvents[i].Header) 771 if _, ok := ev.Event.(*replication.GTIDEvent); ok { 772 gtidStr, _ := event.GetGTIDStr(ev) 773 c.Assert(preGset.Update(gtidStr), IsNil) 774 775 // get pos by preGset 776 pos, err2 := r.getPosByGTID(preGset.Clone()) 777 c.Assert(err2, IsNil) 778 // check result 779 c.Assert(pos.Name, Equals, allResults[gtidEventCount]) 780 c.Assert(pos.Pos, Equals, uint32(4)) 781 gtidEventCount++ 782 } 783 } 784 785 r.Close() 786 r = newBinlogReaderForTest(log.L(), cfg, true, "") 787 788 excludeStrs := []string{} 789 // exclude event except for first server 790 includeServerUUID := testCase[0].serverUUID 791 includeUUID := testCase[0].uuid 792 for _, s := range strings.Split(preGset.String(), ",") { 793 if !strings.Contains(s, includeServerUUID) { 794 excludeStrs = append(excludeStrs, s) 795 } 796 } 797 excludeStr := strings.Join(excludeStrs, ",") 798 excludeGset, err := gmysql.ParseGTIDSet(gmysql.MySQLFlavor, excludeStr) 799 c.Assert(err, IsNil) 800 801 // StartSyncByGtid exclude first uuid 802 s, err = r.StartSyncByGTID(excludeGset) 803 c.Assert(err, IsNil) 804 obtainBaseEvents = readNEvents(ctx, c, s, eventsNumOfFirstServer, true) 805 806 gset := excludeGset.Clone() 807 // should not receive any event not from first server 808 for i, event := range obtainBaseEvents { 809 switch event.Header.EventType { 810 case replication.HEARTBEAT_EVENT: 811 c.FailNow() 812 case replication.GTID_EVENT: 813 // check gtid event comes from first uuid subdir 814 ev, _ := event.Event.(*replication.GTIDEvent) 815 u, _ := uuid.FromBytes(ev.SID) 816 c.Assert(u.String(), Equals, includeServerUUID) 817 c.Assert(event.Header, DeepEquals, allEvents[i].Header) 818 c.Assert(gset.Update(fmt.Sprintf("%s:%d", u.String(), ev.GNO)), IsNil) 819 default: 820 c.Assert(event.Header, DeepEquals, allEvents[i].Header) 821 } 822 } 823 // gset same as preGset now 824 c.Assert(gset.Equal(preGset), IsTrue) 825 826 // purge first uuid subdir's first binlog file 827 c.Assert(os.Remove(path.Join(baseDir, includeUUID, "mysql.000001")), IsNil) 828 829 r.Close() 830 r = newBinlogReaderForTest(log.L(), cfg, true, "") 831 _, err = r.StartSyncByGTID(preGset) 832 c.Assert(err, IsNil) 833 834 r.Close() 835 r = newBinlogReaderForTest(log.L(), cfg, true, "") 836 _, err = r.StartSyncByGTID(excludeGset) 837 // error because file has been purge 838 c.Assert(terror.ErrNoRelayPosMatchGTID.Equal(err), IsTrue) 839 840 // purge first uuid subdir 841 c.Assert(os.RemoveAll(path.Join(baseDir, includeUUID)), IsNil) 842 843 r.Close() 844 r = newBinlogReaderForTest(log.L(), cfg, true, "") 845 _, err = r.StartSyncByGTID(preGset) 846 c.Assert(err, IsNil) 847 848 r.Close() 849 r = newBinlogReaderForTest(log.L(), cfg, true, "") 850 _, err = r.StartSyncByGTID(excludeGset) 851 // error because subdir has been purge 852 c.Assert(err, ErrorMatches, ".*no such file or directory.*") 853 cancel() 854 } 855 856 func (t *testReaderSuite) TestStartSyncError(c *C) { 857 var ( 858 baseDir = c.MkDir() 859 UUIDs = []string{ 860 "b60868af-5a6f-11e9-9ea3-0242ac160006.000001", 861 } 862 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 863 startPos = gmysql.Position{Name: "test-mysql-bin|000001.000001"} // from the first relay log file in the first sub directory 864 ) 865 866 r := newBinlogReaderForTest(log.L(), cfg, true, "") 867 err := r.checkRelayPos(startPos) 868 c.Assert(err, ErrorMatches, ".*empty UUIDs not valid.*") 869 870 // no startup pos specified 871 s, err := r.StartSyncByPos(gmysql.Position{}) 872 c.Assert(terror.ErrBinlogFileNotSpecified.Equal(err), IsTrue) 873 c.Assert(s, IsNil) 874 875 // empty UUIDs 876 s, err = r.StartSyncByPos(startPos) 877 c.Assert(err, ErrorMatches, ".*empty UUIDs not valid.*") 878 c.Assert(s, IsNil) 879 880 s, err = r.StartSyncByGTID(t.lastGTID.Clone()) 881 c.Assert(err, ErrorMatches, ".*no relay pos match gtid.*") 882 c.Assert(s, IsNil) 883 884 // write UUIDs into index file 885 r = newBinlogReaderForTest(log.L(), cfg, true, "") // create a new reader 886 uuidBytes := t.uuidListToBytes(c, UUIDs) 887 err = os.WriteFile(r.indexPath, uuidBytes, 0o600) 888 c.Assert(err, IsNil) 889 890 // the startup relay log file not found 891 s, err = r.StartSyncByPos(startPos) 892 c.Assert(err, ErrorMatches, fmt.Sprintf(".*%s.*not found.*", startPos.Name)) 893 c.Assert(s, IsNil) 894 895 s, err = r.StartSyncByGTID(t.lastGTID.Clone()) 896 c.Assert(err, ErrorMatches, ".*no such file or directory.*") 897 c.Assert(s, IsNil) 898 899 // can not re-start the reader 900 r.running = true 901 s, err = r.StartSyncByPos(startPos) 902 c.Assert(terror.ErrReaderAlreadyRunning.Equal(err), IsTrue) 903 c.Assert(s, IsNil) 904 r.Close() 905 906 r.running = true 907 s, err = r.StartSyncByGTID(t.lastGTID.Clone()) 908 c.Assert(terror.ErrReaderAlreadyRunning.Equal(err), IsTrue) 909 c.Assert(s, IsNil) 910 r.Close() 911 912 // too big startPos 913 uuid := UUIDs[0] 914 err = os.MkdirAll(filepath.Join(baseDir, uuid), 0o700) 915 c.Assert(err, IsNil) 916 parsedStartPosName := "test-mysql-bin.000001" 917 relayLogFilePath := filepath.Join(baseDir, uuid, parsedStartPosName) 918 err = os.WriteFile(relayLogFilePath, make([]byte, 100), 0o600) 919 c.Assert(err, IsNil) 920 startPos.Pos = 10000 921 s, err = r.StartSyncByPos(startPos) 922 c.Assert(terror.ErrRelayLogGivenPosTooBig.Equal(err), IsTrue) 923 c.Assert(s, IsNil) 924 } 925 926 func (t *testReaderSuite) TestAdvanceCurrentGTIDSet(c *C) { 927 var ( 928 baseDir = c.MkDir() 929 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 930 r = newBinlogReaderForTest(log.L(), cfg, true, "") 931 mysqlGset, _ = gmysql.ParseMysqlGTIDSet("b60868af-5a6f-11e9-9ea3-0242ac160006:1-6") 932 mariadbGset, _ = gmysql.ParseMariadbGTIDSet("0-1-5") 933 ) 934 r.prevGset = mysqlGset.Clone() 935 r.currGset = nil 936 notUpdated, err := r.advanceCurrentGtidSet("b60868af-5a6f-11e9-9ea3-0242ac160006:6") 937 c.Assert(err, IsNil) 938 c.Assert(notUpdated, IsTrue) 939 c.Assert(mysqlGset.Equal(r.currGset), IsTrue) 940 notUpdated, err = r.advanceCurrentGtidSet("b60868af-5a6f-11e9-9ea3-0242ac160006:7") 941 c.Assert(err, IsNil) 942 c.Assert(notUpdated, IsFalse) 943 c.Assert(mysqlGset.Equal(r.prevGset), IsTrue) 944 c.Assert(r.currGset.String(), Equals, "b60868af-5a6f-11e9-9ea3-0242ac160006:1-7") 945 946 r.cfg.Flavor = gmysql.MariaDBFlavor 947 r.prevGset = mariadbGset.Clone() 948 r.currGset = nil 949 notUpdated, err = r.advanceCurrentGtidSet("0-1-3") 950 c.Assert(err, IsNil) 951 c.Assert(notUpdated, IsTrue) 952 c.Assert(mariadbGset.Equal(r.currGset), IsTrue) 953 notUpdated, err = r.advanceCurrentGtidSet("0-1-6") 954 c.Assert(err, IsNil) 955 c.Assert(notUpdated, IsFalse) 956 c.Assert(mariadbGset.Equal(r.prevGset), IsTrue) 957 c.Assert(r.currGset.String(), Equals, "0-1-6") 958 } 959 960 func (t *testReaderSuite) TestReParseUsingGTID(c *C) { 961 var ( 962 baseDir = c.MkDir() 963 cfg = &BinlogReaderConfig{RelayDir: baseDir, Flavor: gmysql.MySQLFlavor} 964 r = newBinlogReaderForTest(log.L(), cfg, true, "") 965 uuid = "ba8f633f-1f15-11eb-b1c7-0242ac110002.000001" 966 gtidStr = "ba8f633f-1f15-11eb-b1c7-0242ac110002:1" 967 file = "mysql.000001" 968 latestPos uint32 969 ) 970 971 startGTID, err := gtid.ParserGTID(gmysql.MySQLFlavor, "") 972 c.Assert(err, IsNil) 973 lastGTID, err := gtid.ParserGTID(gmysql.MySQLFlavor, gtidStr) 974 c.Assert(err, IsNil) 975 976 // prepare a minimal relay log file 977 c.Assert(os.WriteFile(r.indexPath, []byte(uuid), 0o600), IsNil) 978 979 uuidDir := path.Join(baseDir, uuid) 980 c.Assert(os.MkdirAll(uuidDir, 0o700), IsNil) 981 f, err := os.OpenFile(path.Join(uuidDir, file), os.O_CREATE|os.O_WRONLY, 0o600) 982 c.Assert(err, IsNil) 983 _, err = f.Write(replication.BinLogFileHeader) 984 c.Assert(err, IsNil) 985 986 meta := LocalMeta{BinLogName: file, BinLogPos: latestPos, BinlogGTID: startGTID.String()} 987 metaFile, err := os.Create(path.Join(uuidDir, utils.MetaFilename)) 988 c.Assert(err, IsNil) 989 c.Assert(toml.NewEncoder(metaFile).Encode(&meta), IsNil) 990 c.Assert(metaFile.Close(), IsNil) 991 992 // prepare some regular events, 993 // FORMAT_DESC + PREVIOUS_GTIDS, some events generated from a DDL, some events generated from a DML 994 genType := []replication.EventType{ 995 replication.PREVIOUS_GTIDS_EVENT, 996 replication.QUERY_EVENT, 997 replication.XID_EVENT, 998 } 999 events, _, _, latestGTIDSet := t.genEvents(c, genType, 4, lastGTID, startGTID) 1000 c.Assert(events, HasLen, 1+1+2+5) 1001 1002 // write FORMAT_DESC + PREVIOUS_GTIDS 1003 _, err = f.Write(events[0].RawData) 1004 c.Assert(err, IsNil) 1005 _, err = f.Write(events[1].RawData) 1006 c.Assert(err, IsNil) 1007 1008 // we use latestGTIDSet to start sync, which means we already received all binlog events, so expect no DML/DDL 1009 s, err := r.StartSyncByGTID(latestGTIDSet) 1010 c.Assert(err, IsNil) 1011 var wg sync.WaitGroup 1012 wg.Add(1) 1013 1014 ctx, cancel := context.WithCancel(context.Background()) 1015 1016 go func() { 1017 expected := map[uint32]replication.EventType{} 1018 for _, e := range events { 1019 // will not receive event for skipped GTID 1020 switch e.Event.(type) { 1021 case *replication.FormatDescriptionEvent, *replication.PreviousGTIDsEvent: 1022 expected[e.Header.LogPos] = e.Header.EventType 1023 } 1024 } 1025 // fake rotate 1026 expected[0] = replication.ROTATE_EVENT 1027 1028 for { 1029 ev, err2 := s.GetEvent(ctx) 1030 if err2 == context.Canceled { 1031 break 1032 } 1033 c.Assert(err2, IsNil) 1034 c.Assert(ev.Header.EventType, Equals, expected[ev.Header.LogPos]) 1035 } 1036 wg.Done() 1037 }() 1038 1039 for i := 2; i < len(events); i++ { 1040 // hope a second is enough to trigger needReParse 1041 time.Sleep(time.Second) 1042 _, err = f.Write(events[i].RawData) 1043 c.Assert(err, IsNil) 1044 _ = f.Sync() 1045 select { 1046 case r.notifyCh <- struct{}{}: 1047 default: 1048 } 1049 } 1050 time.Sleep(time.Second) 1051 cancel() 1052 wg.Wait() 1053 } 1054 1055 func (t *testReaderSuite) genBinlogEvents(c *C, latestPos uint32, latestGTID gmysql.GTIDSet) ([]*replication.BinlogEvent, uint32, gmysql.GTIDSet) { 1056 var ( 1057 header = &replication.EventHeader{ 1058 Timestamp: uint32(time.Now().Unix()), 1059 ServerID: 11, 1060 } 1061 events = make([]*replication.BinlogEvent, 0, 10) 1062 ) 1063 1064 if latestPos <= 4 { // generate a FormatDescriptionEvent if needed 1065 ev, err := event.GenFormatDescriptionEvent(header, 4) 1066 c.Assert(err, IsNil) 1067 latestPos = ev.Header.LogPos 1068 events = append(events, ev) 1069 } 1070 1071 // for these tests, generates some DDL events is enough 1072 count := 5 + rand.Intn(5) 1073 for i := 0; i < count; i++ { 1074 evs, err := event.GenDDLEvents(gmysql.MySQLFlavor, 1, latestPos, latestGTID, fmt.Sprintf("db_%d", i), fmt.Sprintf("CREATE TABLE %d (c1 INT)", i), true, false, 0) 1075 c.Assert(err, IsNil) 1076 events = append(events, evs.Events...) 1077 latestPos = evs.LatestPos 1078 latestGTID = evs.LatestGTID 1079 } 1080 1081 return events, latestPos, latestGTID 1082 } 1083 1084 func (t *testReaderSuite) genEvents( 1085 c *C, 1086 eventTypes []replication.EventType, 1087 latestPos uint32, 1088 latestGTID gmysql.GTIDSet, 1089 previousGset gmysql.GTIDSet, 1090 ) ([]*replication.BinlogEvent, uint32, gmysql.GTIDSet, gmysql.GTIDSet) { 1091 var ( 1092 header = &replication.EventHeader{ 1093 Timestamp: uint32(time.Now().Unix()), 1094 ServerID: 11, 1095 } 1096 events = make([]*replication.BinlogEvent, 0, 10) 1097 pGset = previousGset.Clone() 1098 originSet = pGset 1099 ) 1100 1101 if latestPos <= 4 { // generate a FormatDescriptionEvent if needed 1102 ev, err := event.GenFormatDescriptionEvent(header, 4) 1103 c.Assert(err, IsNil) 1104 latestPos = ev.Header.LogPos 1105 events = append(events, ev) 1106 } 1107 1108 for i, eventType := range eventTypes { 1109 switch eventType { 1110 case replication.QUERY_EVENT: 1111 evs, err := event.GenDDLEvents(gmysql.MySQLFlavor, 1, latestPos, latestGTID, fmt.Sprintf("db_%d", i), fmt.Sprintf("CREATE TABLE %d (c1 int)", i), true, false, 0) 1112 c.Assert(err, IsNil) 1113 events = append(events, evs.Events...) 1114 latestPos = evs.LatestPos 1115 latestGTID = evs.LatestGTID 1116 _, ok := evs.Events[0].Event.(*replication.GTIDEvent) 1117 c.Assert(ok, IsTrue) 1118 gtidStr, _ := event.GetGTIDStr(evs.Events[0]) 1119 err = originSet.Update(gtidStr) 1120 c.Assert(err, IsNil) 1121 case replication.XID_EVENT: 1122 insertDMLData := []*event.DMLData{ 1123 { 1124 TableID: uint64(i), 1125 Schema: fmt.Sprintf("db_%d", i), 1126 Table: strconv.Itoa(i), 1127 ColumnType: []byte{gmysql.MYSQL_TYPE_INT24}, 1128 Rows: [][]interface{}{{int32(1)}, {int32(2)}}, 1129 }, 1130 } 1131 evs, err := event.GenDMLEvents(gmysql.MySQLFlavor, 1, latestPos, latestGTID, replication.WRITE_ROWS_EVENTv2, 10, insertDMLData, true, false, 0) 1132 c.Assert(err, IsNil) 1133 events = append(events, evs.Events...) 1134 latestPos = evs.LatestPos 1135 latestGTID = evs.LatestGTID 1136 _, ok := evs.Events[0].Event.(*replication.GTIDEvent) 1137 c.Assert(ok, IsTrue) 1138 gtidStr, _ := event.GetGTIDStr(evs.Events[0]) 1139 err = originSet.Update(gtidStr) 1140 c.Assert(err, IsNil) 1141 case replication.ROTATE_EVENT: 1142 ev, err := event.GenRotateEvent(header, latestPos, []byte("next_log"), 4) 1143 c.Assert(err, IsNil) 1144 events = append(events, ev) 1145 latestPos = 4 1146 case replication.PREVIOUS_GTIDS_EVENT: 1147 ev, err := event.GenPreviousGTIDsEvent(header, latestPos, pGset) 1148 c.Assert(err, IsNil) 1149 events = append(events, ev) 1150 latestPos = ev.Header.LogPos 1151 } 1152 } 1153 return events, latestPos, latestGTID, originSet 1154 } 1155 1156 func (t *testReaderSuite) purgeStreamer(c *C, s reader.Streamer) { 1157 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 1158 defer cancel() 1159 1160 for { 1161 _, err := s.GetEvent(ctx) 1162 switch { 1163 case err == nil: 1164 continue 1165 case err == ctx.Err(): 1166 return 1167 default: 1168 c.Fatalf("purge streamer with error %v", err) 1169 } 1170 } 1171 } 1172 1173 func (t *testReaderSuite) verifyNoEventsInStreamer(c *C, s reader.Streamer) { 1174 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 1175 defer cancel() 1176 1177 ev, err := s.GetEvent(ctx) 1178 if err != ctx.Err() { 1179 c.Fatalf("got event %v with error %v from streamer", ev, err) 1180 } 1181 } 1182 1183 func (t *testReaderSuite) uuidListToBytes(c *C, uuids []string) []byte { 1184 var buf bytes.Buffer 1185 for _, uuid := range uuids { 1186 _, err := buf.WriteString(uuid) 1187 c.Assert(err, IsNil) 1188 _, err = buf.WriteString("\n") 1189 c.Assert(err, IsNil) 1190 } 1191 return buf.Bytes() 1192 } 1193 1194 // nolint:unparam 1195 func (t *testReaderSuite) writeUUIDs(c *C, relayDir string, uuids []string) []byte { 1196 indexPath := path.Join(relayDir, utils.UUIDIndexFilename) 1197 var buf bytes.Buffer 1198 for _, uuid := range uuids { 1199 _, err := buf.WriteString(uuid) 1200 c.Assert(err, IsNil) 1201 _, err = buf.WriteString("\n") 1202 c.Assert(err, IsNil) 1203 } 1204 1205 // write the index file 1206 err := os.WriteFile(indexPath, buf.Bytes(), 0o600) 1207 c.Assert(err, IsNil) 1208 return buf.Bytes() 1209 } 1210 1211 func (t *testReaderSuite) createMetaFile(c *C, relayDirPath, binlogFileName string, pos uint32, gtid string) { 1212 meta := LocalMeta{BinLogName: binlogFileName, BinLogPos: pos, BinlogGTID: gtid} 1213 metaFile, err2 := os.Create(path.Join(relayDirPath, utils.MetaFilename)) 1214 c.Assert(err2, IsNil) 1215 err := toml.NewEncoder(metaFile).Encode(&meta) 1216 c.Assert(err, IsNil) 1217 metaFile.Close() 1218 } 1219 1220 type mockActiveCase struct { 1221 active bool 1222 offset int64 1223 } 1224 1225 type mockFileWriterForActiveTest struct { 1226 cnt int 1227 cases []mockActiveCase 1228 } 1229 1230 func (m *mockFileWriterForActiveTest) Init(uuid, filename string) { 1231 panic("should be used") 1232 } 1233 1234 func (m *mockFileWriterForActiveTest) Close() error { 1235 panic("should be used") 1236 } 1237 1238 func (m *mockFileWriterForActiveTest) Flush() error { 1239 panic("should be used") 1240 } 1241 1242 func (m *mockFileWriterForActiveTest) WriteEvent(ev *replication.BinlogEvent) (WResult, error) { 1243 panic("should be used") 1244 } 1245 1246 func (m *mockFileWriterForActiveTest) IsActive(uuid, filename string) (bool, int64) { 1247 v := m.cases[m.cnt] 1248 m.cnt++ 1249 return v.active, v.offset 1250 } 1251 1252 func (t *testReaderSuite) TestwaitBinlogChanged(c *C) { 1253 var ( 1254 relayFiles = []string{ 1255 "mysql-bin.000001", 1256 "mysql-bin.000002", 1257 } 1258 binlogPos = uint32(4) 1259 binlogGTID = "ba8f633f-1f15-11eb-b1c7-0242ac110002:1" 1260 relayPaths = make([]string, len(relayFiles)) 1261 data = []byte("meaningless file content") 1262 size = int64(len(data)) 1263 ) 1264 1265 // create relay log dir 1266 subDir := c.MkDir() 1267 // join the file path 1268 for i, rf := range relayFiles { 1269 relayPaths[i] = filepath.Join(subDir, rf) 1270 f, _ := os.Create(relayPaths[i]) 1271 _ = f.Close() 1272 } 1273 1274 rotateRelayFile := func(filename string) { 1275 meta := LocalMeta{BinLogName: filename, BinLogPos: binlogPos, BinlogGTID: binlogGTID} 1276 metaFile, err2 := os.Create(path.Join(subDir, utils.MetaFilename)) 1277 c.Assert(err2, IsNil) 1278 err := toml.NewEncoder(metaFile).Encode(&meta) 1279 c.Assert(err, IsNil) 1280 _ = metaFile.Close() 1281 } 1282 1283 // meta not found 1284 { 1285 cfg := &BinlogReaderConfig{RelayDir: "", Flavor: gmysql.MySQLFlavor} 1286 r := newBinlogReaderForTest(log.L(), cfg, false, "") 1287 t.setActiveRelayLog(r.relay, "next", "next", 0) 1288 state := t.createBinlogFileParseState(c, subDir, relayFiles[0], 0, true) 1289 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1290 c.Assert(needSwitch, IsFalse) 1291 c.Assert(reParse, IsFalse) 1292 c.Assert(err, NotNil) 1293 c.Assert(err, ErrorMatches, ".*no such file or directory*") 1294 } 1295 1296 // write meta 1297 rotateRelayFile(relayFiles[0]) 1298 1299 // relay file not found 1300 { 1301 cfg := &BinlogReaderConfig{RelayDir: "", Flavor: gmysql.MySQLFlavor} 1302 r := newBinlogReaderForTest(log.L(), cfg, false, "") 1303 t.setActiveRelayLog(r.relay, "next", "next", 0) 1304 state := &binlogFileParseState{ 1305 relayLogDir: subDir, 1306 relayLogFile: "not-exist-file", 1307 } 1308 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1309 c.Assert(needSwitch, IsFalse) 1310 c.Assert(reParse, IsFalse) 1311 c.Assert(err, NotNil) 1312 c.Assert(err, ErrorMatches, ".*no such file or directory*") 1313 } 1314 1315 // create the first relay file 1316 err1 := os.WriteFile(relayPaths[0], data, 0o600) 1317 c.Assert(err1, IsNil) 1318 // rotate relay file 1319 rotateRelayFile(relayFiles[1]) 1320 1321 // file decreased when meta changed 1322 { 1323 cfg := &BinlogReaderConfig{RelayDir: "", Flavor: gmysql.MySQLFlavor} 1324 r := newBinlogReaderForTest(log.L(), cfg, false, "") 1325 t.setActiveRelayLog(r.relay, "next", "next", 0) 1326 state := t.createBinlogFileParseState(c, subDir, relayFiles[0], size+100, true) 1327 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1328 c.Assert(needSwitch, IsFalse) 1329 c.Assert(reParse, IsFalse) 1330 c.Assert(err, NotNil) 1331 c.Assert(terror.ErrRelayLogFileSizeSmaller.Equal(err), IsTrue) 1332 } 1333 1334 // return changed file in meta 1335 { 1336 cfg := &BinlogReaderConfig{RelayDir: "", Flavor: gmysql.MySQLFlavor} 1337 r := newBinlogReaderForTest(log.L(), cfg, false, "") 1338 t.setActiveRelayLog(r.relay, "next", "next", 0) 1339 state := t.createBinlogFileParseState(c, subDir, relayFiles[0], size, true) 1340 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1341 c.Assert(needSwitch, IsFalse) 1342 c.Assert(reParse, IsFalse) 1343 c.Assert(err, IsNil) 1344 } 1345 1346 // file increased when checking meta 1347 { 1348 cfg := &BinlogReaderConfig{RelayDir: "", Flavor: gmysql.MySQLFlavor} 1349 r := newBinlogReaderForTest(log.L(), cfg, false, "") 1350 t.setActiveRelayLog(r.relay, "next", "next", 0) 1351 state := t.createBinlogFileParseState(c, subDir, relayFiles[0], 0, true) 1352 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1353 c.Assert(needSwitch, IsFalse) 1354 c.Assert(reParse, IsTrue) 1355 c.Assert(err, IsNil) 1356 } 1357 1358 // context timeout (no new write) 1359 { 1360 newCtx, cancel := context.WithTimeout(context.Background(), time.Millisecond) 1361 defer cancel() 1362 cfg := &BinlogReaderConfig{RelayDir: "", Flavor: gmysql.MySQLFlavor} 1363 r := newBinlogReaderForTest(log.L(), cfg, false, "current-uuid") // no notify 1364 t.setActiveRelayLog(r.relay, "current-uuid", relayFiles[0], 0) 1365 state := t.createBinlogFileParseState(c, subDir, relayFiles[0], 0, true) 1366 needSwitch, reParse, err := r.waitBinlogChanged(newCtx, state) 1367 c.Assert(needSwitch, IsFalse) 1368 c.Assert(reParse, IsFalse) 1369 c.Assert(err, IsNil) 1370 } 1371 1372 // this dir is different from the dir of current binlog file, but for test it doesn't matter 1373 relayDir := c.MkDir() 1374 t.writeUUIDs(c, relayDir, []string{"xxx.000001", "invalid uuid"}) 1375 1376 // getSwitchPath return error(invalid uuid file) 1377 { 1378 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1379 r := newBinlogReaderForTest(log.L(), cfg, false, "xxx.000001") 1380 t.setActiveRelayLog(r.relay, "next", "next", 0) 1381 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], 0, true) 1382 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1383 c.Assert(needSwitch, IsFalse) 1384 c.Assert(reParse, IsFalse) 1385 c.Assert(terror.ErrRelayParseUUIDSuffix.Equal(err), IsTrue) 1386 } 1387 1388 t.writeUUIDs(c, relayDir, []string{"xxx.000001", "xxx.000002"}) 1389 _ = os.MkdirAll(filepath.Join(relayDir, "xxx.000002"), 0o700) 1390 _ = os.WriteFile(filepath.Join(relayDir, "xxx.000002", "mysql.000001"), nil, 0o600) 1391 1392 // binlog dir switched, but last file not exists so failed to check change of file length 1393 // should not happen in real, just for branch test 1394 { 1395 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1396 r := newBinlogReaderForTest(log.L(), cfg, false, "xxx.000001") 1397 t.setActiveRelayLog(r.relay, "next", "next", 0) 1398 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], 0, true) 1399 _ = os.Remove(relayPaths[1]) 1400 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1401 c.Assert(needSwitch, IsFalse) 1402 c.Assert(reParse, IsFalse) 1403 c.Assert(terror.ErrGetRelayLogStat.Equal(err), IsTrue) 1404 } 1405 1406 err1 = os.WriteFile(relayPaths[1], nil, 0o600) 1407 c.Assert(err1, IsNil) 1408 1409 // binlog dir switched, but last file smaller 1410 { 1411 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1412 r := newBinlogReaderForTest(log.L(), cfg, false, "xxx.000001") 1413 t.setActiveRelayLog(r.relay, "next", "next", 0) 1414 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], size, true) 1415 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1416 c.Assert(needSwitch, IsFalse) 1417 c.Assert(reParse, IsFalse) 1418 c.Assert(terror.ErrRelayLogFileSizeSmaller.Equal(err), IsTrue) 1419 } 1420 1421 err1 = os.WriteFile(relayPaths[1], data, 0o600) 1422 c.Assert(err1, IsNil) 1423 1424 // binlog dir switched, but last file bigger 1425 { 1426 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1427 r := newBinlogReaderForTest(log.L(), cfg, false, "xxx.000001") 1428 t.setActiveRelayLog(r.relay, "next", "next", 0) 1429 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], 0, true) 1430 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1431 c.Assert(needSwitch, IsFalse) 1432 c.Assert(reParse, IsTrue) 1433 c.Assert(err, IsNil) 1434 } 1435 1436 // binlog dir switched, but last file not changed 1437 { 1438 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1439 r := newBinlogReaderForTest(log.L(), cfg, false, "xxx.000001") 1440 t.setActiveRelayLog(r.relay, "next", "next", 0) 1441 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], size, true) 1442 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1443 c.Assert(needSwitch, IsTrue) 1444 c.Assert(reParse, IsFalse) 1445 c.Assert(err, IsNil) 1446 } 1447 1448 // got notified and active pos > current read pos 1449 { 1450 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1451 r := newBinlogReaderForTest(log.L(), cfg, true, "xxx.000001") 1452 t.setActiveRelayLog(r.relay, r.currentSubDir, relayFiles[1], size) 1453 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], 0, true) 1454 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1455 c.Assert(needSwitch, IsFalse) 1456 c.Assert(reParse, IsTrue) 1457 c.Assert(err, IsNil) 1458 } 1459 1460 // got notified but not active 1461 { 1462 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1463 r := newBinlogReaderForTest(log.L(), cfg, true, "xxx.000001") 1464 relay := r.relay.(*Relay) 1465 relay.writer = &mockFileWriterForActiveTest{cases: []mockActiveCase{ 1466 {true, 0}, 1467 {false, 0}, 1468 }} 1469 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], 0, true) 1470 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1471 c.Assert(needSwitch, IsFalse) 1472 c.Assert(reParse, IsTrue) 1473 c.Assert(err, IsNil) 1474 } 1475 1476 // got notified, first notified is active but has already read to that offset 1477 // second notify, got new data 1478 { 1479 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1480 r := newBinlogReaderForTest(log.L(), cfg, true, "xxx.000001") 1481 r.notifyCh = make(chan interface{}, 2) 1482 r.notifyCh <- struct{}{} 1483 r.notifyCh <- struct{}{} 1484 relay := r.relay.(*Relay) 1485 relay.writer = &mockFileWriterForActiveTest{cases: []mockActiveCase{ 1486 {true, 0}, 1487 {true, 0}, 1488 {true, size}, 1489 }} 1490 state := t.createBinlogFileParseState(c, subDir, relayFiles[1], 0, true) 1491 needSwitch, reParse, err := r.waitBinlogChanged(context.Background(), state) 1492 c.Assert(needSwitch, IsFalse) 1493 c.Assert(reParse, IsTrue) 1494 c.Assert(err, IsNil) 1495 } 1496 } 1497 1498 func (t *testReaderSuite) TestGetSwitchPath(c *C) { 1499 var ( 1500 relayDir = c.MkDir() 1501 UUIDs = []string{ 1502 "53ea0ed1-9bf8-11e6-8bea-64006a897c73.000001", 1503 "53ea0ed1-9bf8-11e6-8bea-64006a897c72.000002", 1504 "53ea0ed1-9bf8-11e6-8bea-64006a897c71.000003", 1505 } 1506 currentUUID = UUIDs[len(UUIDs)-1] // no next UUID 1507 ) 1508 1509 UUIDs = append(UUIDs, "invalid.uuid") 1510 1511 // invalid UUID in UUIDs, error 1512 t.writeUUIDs(c, relayDir, UUIDs) 1513 { 1514 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1515 r := newBinlogReaderForTest(log.L(), cfg, true, currentUUID) 1516 switchPath, err := r.getSwitchPath() 1517 c.Assert(switchPath, IsNil) 1518 c.Assert(terror.ErrRelayParseUUIDSuffix.Equal(err), IsTrue) 1519 } 1520 1521 UUIDs = UUIDs[:len(UUIDs)-1] // remove the invalid UUID 1522 t.writeUUIDs(c, relayDir, UUIDs) 1523 1524 // no next sub directory 1525 { 1526 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1527 r := newBinlogReaderForTest(log.L(), cfg, true, UUIDs[0]) 1528 switchPath, err := r.getSwitchPath() 1529 c.Assert(switchPath, IsNil) 1530 c.Assert(err, ErrorMatches, fmt.Sprintf(".*%s.*(no such file or directory|The system cannot find the file specified).*", UUIDs[1])) 1531 } 1532 1533 err1 := os.Mkdir(filepath.Join(relayDir, UUIDs[1]), 0o700) 1534 c.Assert(err1, IsNil) 1535 1536 // uuid directory exist, but no binlog file inside 1537 { 1538 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1539 r := newBinlogReaderForTest(log.L(), cfg, true, UUIDs[0]) 1540 switchPath, err := r.getSwitchPath() 1541 c.Assert(switchPath, IsNil) 1542 c.Assert(err, IsNil) 1543 } 1544 1545 // create a relay log file in the next sub directory 1546 nextBinlogPath := filepath.Join(relayDir, UUIDs[1], "mysql-bin.000001") 1547 err1 = os.MkdirAll(filepath.Dir(nextBinlogPath), 0o700) 1548 c.Assert(err1, IsNil) 1549 err1 = os.WriteFile(nextBinlogPath, nil, 0o600) 1550 c.Assert(err1, IsNil) 1551 1552 // switch to the next 1553 { 1554 cfg := &BinlogReaderConfig{RelayDir: relayDir, Flavor: gmysql.MySQLFlavor} 1555 r := newBinlogReaderForTest(log.L(), cfg, true, UUIDs[0]) 1556 switchPath, err := r.getSwitchPath() 1557 c.Assert(switchPath.nextUUID, Equals, UUIDs[1]) 1558 c.Assert(switchPath.nextBinlogName, Equals, filepath.Base(nextBinlogPath)) 1559 c.Assert(err, IsNil) 1560 } 1561 }