github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/relay/local_reader.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 "context" 18 "io" 19 "os" 20 "path" 21 "path/filepath" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/BurntSushi/toml" 27 "github.com/go-mysql-org/go-mysql/mysql" 28 "github.com/go-mysql-org/go-mysql/replication" 29 "github.com/pingcap/errors" 30 "github.com/pingcap/tiflow/dm/pkg/binlog" 31 "github.com/pingcap/tiflow/dm/pkg/binlog/event" 32 "github.com/pingcap/tiflow/dm/pkg/binlog/reader" 33 tcontext "github.com/pingcap/tiflow/dm/pkg/context" 34 "github.com/pingcap/tiflow/dm/pkg/log" 35 "github.com/pingcap/tiflow/dm/pkg/terror" 36 "github.com/pingcap/tiflow/dm/pkg/utils" 37 "go.uber.org/zap" 38 ) 39 40 // ErrorMaybeDuplicateEvent indicates that there may be duplicate event in next binlog file 41 // this is mainly happened when upstream master changed when relay log not finish reading a transaction. 42 var ErrorMaybeDuplicateEvent = errors.New("truncate binlog file found, event may be duplicated") 43 44 // BinlogReaderConfig is the configuration for BinlogReader. 45 type BinlogReaderConfig struct { 46 RelayDir string 47 Timezone *time.Location 48 Flavor string 49 RowsEventDecodeFunc func(*replication.RowsEvent, []byte) error 50 } 51 52 // BinlogReader is a binlog reader. 53 type BinlogReader struct { 54 cfg *BinlogReaderConfig 55 parser *replication.BinlogParser 56 57 indexPath string // relay server-uuid index file path 58 subDirs []string 59 60 latestServerID uint32 // latest server ID, got from relay log 61 62 running bool 63 wg sync.WaitGroup 64 cancel context.CancelFunc 65 66 tctx *tcontext.Context 67 68 usingGTID bool 69 prevGset, currGset mysql.GTIDSet 70 // ch with size = 1, we only need to be notified whether binlog file of relay changed, not how many times 71 notifyCh chan interface{} 72 relay Process 73 74 currentSubDir string // current UUID(with suffix) 75 76 lastFileGracefulEnd bool 77 } 78 79 // newBinlogReader creates a new BinlogReader. 80 func newBinlogReader(logger log.Logger, cfg *BinlogReaderConfig, relay Process) *BinlogReader { 81 ctx, cancel := context.WithCancel(context.Background()) // only can be canceled in `Close` 82 parser := replication.NewBinlogParser() 83 parser.SetVerifyChecksum(true) 84 // use string representation of decimal, to replicate the exact value 85 parser.SetUseDecimal(false) 86 parser.SetRowsEventDecodeFunc(cfg.RowsEventDecodeFunc) 87 if cfg.Timezone != nil { 88 parser.SetTimestampStringLocation(cfg.Timezone) 89 } 90 91 newtctx := tcontext.NewContext(ctx, logger.WithFields(zap.String("component", "binlog reader"))) 92 93 binlogReader := &BinlogReader{ 94 cfg: cfg, 95 parser: parser, 96 indexPath: path.Join(cfg.RelayDir, utils.UUIDIndexFilename), 97 cancel: cancel, 98 tctx: newtctx, 99 notifyCh: make(chan interface{}, 1), 100 relay: relay, 101 lastFileGracefulEnd: true, 102 } 103 binlogReader.relay.RegisterListener(binlogReader) 104 return binlogReader 105 } 106 107 // checkRelayPos will check whether the given relay pos is too big. 108 func (r *BinlogReader) checkRelayPos(pos mysql.Position) error { 109 currentSubDir, _, realPos, err := binlog.ExtractPos(pos, r.subDirs) 110 if err != nil { 111 return terror.Annotatef(err, "parse relay dir with pos %s", pos) 112 } 113 pos = realPos 114 relayFilepath := path.Join(r.cfg.RelayDir, currentSubDir, pos.Name) 115 r.tctx.L().Info("start to check relay log file", zap.String("path", relayFilepath), zap.Stringer("position", pos)) 116 fi, err := os.Stat(relayFilepath) 117 if err != nil { 118 return terror.ErrGetRelayLogStat.Delegate(err, relayFilepath) 119 } 120 if fi.Size() < int64(pos.Pos) { 121 return terror.ErrRelayLogGivenPosTooBig.Generate(pos) 122 } 123 return nil 124 } 125 126 // IsGTIDCoverPreviousFiles check whether gset contains file's previous_gset. 127 func (r *BinlogReader) IsGTIDCoverPreviousFiles(ctx context.Context, filePath string, gset mysql.GTIDSet) (bool, error) { 128 fileReader := reader.NewFileReader(&reader.FileReaderConfig{Timezone: r.cfg.Timezone}) 129 defer fileReader.Close() 130 err := fileReader.StartSyncByPos(mysql.Position{Name: filePath, Pos: binlog.FileHeaderLen}) 131 if err != nil { 132 return false, err 133 } 134 135 var gs mysql.GTIDSet 136 137 for { 138 select { 139 case <-ctx.Done(): 140 return false, nil 141 default: 142 } 143 144 ctx2, cancel := context.WithTimeout(ctx, time.Second) 145 e, err := fileReader.GetEvent(ctx2) 146 cancel() 147 if err != nil { 148 // reach end of file 149 // Maybe we can only Parse the first three fakeRotate, Format_desc and Previous_gtids events. 150 if terror.ErrReaderReachEndOfFile.Equal(err) { 151 return false, terror.ErrPreviousGTIDNotExist.Generate(filePath) 152 } 153 return false, err 154 } 155 156 switch { 157 case e.Header.EventType == replication.PREVIOUS_GTIDS_EVENT: 158 gs, err = event.GTIDsFromPreviousGTIDsEvent(e) 159 case e.Header.EventType == replication.MARIADB_GTID_LIST_EVENT: 160 gs, err = event.GTIDsFromMariaDBGTIDListEvent(e) 161 default: 162 continue 163 } 164 165 if err != nil { 166 return false, err 167 } 168 return gset.Contain(gs), nil 169 } 170 } 171 172 // getPosByGTID gets file position by gtid, result should be (filename, 4). 173 func (r *BinlogReader) getPosByGTID(gset mysql.GTIDSet) (*mysql.Position, error) { 174 // start from newest uuid dir 175 for i := len(r.subDirs) - 1; i >= 0; i-- { 176 subDir := r.subDirs[i] 177 _, suffix, err := utils.ParseRelaySubDir(subDir) 178 if err != nil { 179 return nil, err 180 } 181 182 dir := path.Join(r.cfg.RelayDir, subDir) 183 allFiles, err := CollectAllBinlogFiles(dir) 184 if err != nil { 185 return nil, err 186 } 187 188 // iterate files from the newest one 189 for i := len(allFiles) - 1; i >= 0; i-- { 190 file := allFiles[i] 191 filePath := path.Join(dir, file) 192 // if input `gset` not contain previous_gtids_event's gset (complementary set of `gset` overlap with 193 // previous_gtids_event), that means there're some needed events in previous files. 194 // so we go to previous one 195 contain, err := r.IsGTIDCoverPreviousFiles(r.tctx.Ctx, filePath, gset) 196 if err != nil { 197 return nil, err 198 } 199 if contain { 200 fileName, err := utils.ParseFilename(file) 201 if err != nil { 202 return nil, err 203 } 204 // Start at the beginning of the file 205 return &mysql.Position{ 206 Name: utils.ConstructFilenameWithUUIDSuffix(fileName, utils.SuffixIntToStr(suffix)), 207 Pos: binlog.FileHeaderLen, 208 }, nil 209 } 210 } 211 } 212 return nil, terror.ErrNoRelayPosMatchGTID.Generate(gset.String()) 213 } 214 215 // StartSyncByPos start sync by pos 216 // TODO: thread-safe? 217 func (r *BinlogReader) StartSyncByPos(pos mysql.Position) (reader.Streamer, error) { 218 if pos.Name == "" { 219 return nil, terror.ErrBinlogFileNotSpecified.Generate() 220 } 221 if r.running { 222 return nil, terror.ErrReaderAlreadyRunning.Generate() 223 } 224 225 // load and update UUID list 226 // NOTE: if want to support auto master-slave switching, then needing to re-load UUIDs when parsing. 227 err := r.updateSubDirs() 228 if err != nil { 229 return nil, err 230 } 231 err = r.checkRelayPos(pos) 232 if err != nil { 233 return nil, err 234 } 235 236 r.latestServerID = 0 237 r.running = true 238 s := newLocalStreamer() 239 240 r.wg.Add(1) 241 go func() { 242 defer r.wg.Done() 243 r.tctx.L().Info("start reading", zap.Stringer("position", pos)) 244 err = r.parseRelay(r.tctx.Context(), s, pos) 245 if errors.Cause(err) == r.tctx.Context().Err() { 246 r.tctx.L().Warn("parse relay finished", log.ShortError(err)) 247 } else if err != nil { 248 s.closeWithError(err) 249 r.tctx.L().Error("parse relay stopped", zap.Error(err)) 250 } 251 }() 252 253 return s, nil 254 } 255 256 // StartSyncByGTID start sync by gtid. 257 func (r *BinlogReader) StartSyncByGTID(gset mysql.GTIDSet) (reader.Streamer, error) { 258 r.tctx.L().Info("begin to sync binlog", zap.Stringer("GTID Set", gset)) 259 r.usingGTID = true 260 261 if r.running { 262 return nil, terror.ErrReaderAlreadyRunning.Generate() 263 } 264 265 if err := r.updateSubDirs(); err != nil { 266 return nil, err 267 } 268 269 pos, err := r.getPosByGTID(gset) 270 if err != nil { 271 return nil, err 272 } 273 r.tctx.L().Info("get pos by gtid", zap.Stringer("GTID Set", gset), zap.Stringer("Position", pos)) 274 275 r.prevGset = gset 276 r.currGset = nil 277 278 r.latestServerID = 0 279 r.running = true 280 s := newLocalStreamer() 281 282 r.wg.Add(1) 283 go func() { 284 defer r.wg.Done() 285 r.tctx.L().Info("start reading", zap.Stringer("position", pos)) 286 err = r.parseRelay(r.tctx.Context(), s, *pos) 287 if errors.Cause(err) == r.tctx.Context().Err() { 288 r.tctx.L().Warn("parse relay finished", log.ShortError(err)) 289 } else if err != nil { 290 s.closeWithError(err) 291 r.tctx.L().Error("parse relay stopped", zap.Error(err)) 292 } 293 }() 294 295 return s, nil 296 } 297 298 // SwitchPath represents next binlog file path which should be switched. 299 type SwitchPath struct { 300 nextUUID string 301 nextBinlogName string 302 } 303 304 // parseRelay parses relay root directory, it supports master-slave switch (switching to next sub directory). 305 func (r *BinlogReader) parseRelay(ctx context.Context, s *LocalStreamer, pos mysql.Position) error { 306 currentSubDir, _, realPos, err := binlog.ExtractPos(pos, r.subDirs) 307 if err != nil { 308 return terror.Annotatef(err, "parse relay dir with pos %v", pos) 309 } 310 r.currentSubDir = currentSubDir 311 for { 312 select { 313 case <-ctx.Done(): 314 return ctx.Err() 315 default: 316 } 317 needSwitch, err := r.parseDirAsPossible(ctx, s, realPos) 318 if err != nil { 319 return err 320 } 321 if !needSwitch { 322 return terror.ErrNoSubdirToSwitch.Generate() 323 } 324 // update new uuid 325 if err = r.updateSubDirs(); err != nil { 326 return err 327 } 328 switchPath, err := r.getSwitchPath() 329 if err != nil { 330 return err 331 } 332 if switchPath == nil { 333 // should not happen, we have just called it inside parseDirAsPossible successfully. 334 return errors.New("failed to get switch path") 335 } 336 337 r.currentSubDir = switchPath.nextUUID 338 // update pos, so can switch to next sub directory 339 realPos.Name = switchPath.nextBinlogName 340 realPos.Pos = binlog.FileHeaderLen // start from pos 4 for next sub directory / file 341 r.tctx.L().Info("switching to next ready sub directory", zap.String("next uuid", r.currentSubDir), zap.Stringer("position", pos)) 342 343 // when switching subdirectory, last binlog file may contain unfinished transaction, so we send a notification. 344 if !r.lastFileGracefulEnd { 345 s.ch <- &replication.BinlogEvent{ 346 RawData: []byte(ErrorMaybeDuplicateEvent.Error()), 347 Header: &replication.EventHeader{ 348 EventType: replication.IGNORABLE_EVENT, 349 }, 350 } 351 } 352 } 353 } 354 355 func (r *BinlogReader) getSwitchPath() (*SwitchPath, error) { 356 // reload uuid 357 subDirs, err := utils.ParseUUIDIndex(r.indexPath) 358 if err != nil { 359 return nil, err 360 } 361 nextSubDir, _, err := getNextRelaySubDir(r.currentSubDir, subDirs) 362 if err != nil { 363 return nil, err 364 } 365 if len(nextSubDir) == 0 { 366 return nil, nil 367 } 368 369 // try to get the first binlog file in next subdirectory 370 nextBinlogName, err := getFirstBinlogName(r.cfg.RelayDir, nextSubDir) 371 if err != nil { 372 // because creating subdirectory and writing relay log file are not atomic 373 if terror.ErrBinlogFilesNotFound.Equal(err) { 374 return nil, nil 375 } 376 return nil, err 377 } 378 379 return &SwitchPath{nextSubDir, nextBinlogName}, nil 380 } 381 382 // parseDirAsPossible parses relay subdirectory as far as possible. 383 func (r *BinlogReader) parseDirAsPossible(ctx context.Context, s *LocalStreamer, pos mysql.Position) (needSwitch bool, err error) { 384 firstParse := true // the first parse time for the relay log file 385 dir := path.Join(r.cfg.RelayDir, r.currentSubDir) 386 r.tctx.L().Info("start to parse relay log files in sub directory", zap.String("directory", dir), zap.Stringer("position", pos)) 387 388 for { 389 select { 390 case <-ctx.Done(): 391 return false, ctx.Err() 392 default: 393 } 394 files, err := CollectBinlogFilesCmp(dir, pos.Name, FileCmpBiggerEqual) 395 if err != nil { 396 return false, terror.Annotatef(err, "parse relay dir %s with pos %s", dir, pos) 397 } else if len(files) == 0 { 398 return false, terror.ErrNoRelayLogMatchPos.Generate(dir, pos) 399 } 400 401 r.tctx.L().Debug("start read relay log files", zap.Strings("files", files), zap.String("directory", dir), zap.Stringer("position", pos)) 402 403 var ( 404 latestPos int64 405 latestName string 406 offset = int64(pos.Pos) 407 ) 408 // TODO will this happen? 409 // previously, we use ParseFile which will handle offset < 4, now we use ParseReader which won't 410 if offset < binlog.FileHeaderLen { 411 offset = binlog.FileHeaderLen 412 } 413 for i, relayLogFile := range files { 414 select { 415 case <-ctx.Done(): 416 return false, ctx.Err() 417 default: 418 } 419 if i == 0 { 420 if !strings.HasSuffix(relayLogFile, pos.Name) { 421 return false, terror.ErrFirstRelayLogNotMatchPos.Generate(relayLogFile, pos) 422 } 423 } else { 424 offset = binlog.FileHeaderLen // for other relay log file, start parse from 4 425 firstParse = true // new relay log file need to parse 426 } 427 needSwitch, latestPos, err = r.parseFileAsPossible(ctx, s, relayLogFile, offset, dir, firstParse, i == len(files)-1) 428 if err != nil { 429 return false, terror.Annotatef(err, "parse relay log file %s from offset %d in dir %s", relayLogFile, offset, dir) 430 } 431 firstParse = false // already parsed 432 if needSwitch { 433 // need switch to next relay sub directory 434 return true, nil 435 } 436 latestName = relayLogFile // record the latest file name 437 } 438 439 // update pos, so can re-collect files from the latest file and re start parse from latest pos 440 pos.Pos = uint32(latestPos) 441 pos.Name = latestName 442 } 443 } 444 445 type binlogFileParseState struct { 446 // readonly states 447 possibleLast bool 448 fullPath string 449 relayLogFile, relayLogDir string 450 451 f *os.File 452 453 // states may change 454 skipGTID bool 455 lastSkipGTIDHeader *replication.EventHeader 456 formatDescEventRead bool 457 latestPos int64 458 } 459 460 // parseFileAsPossible parses single relay log file as far as possible. 461 func (r *BinlogReader) parseFileAsPossible(ctx context.Context, s *LocalStreamer, relayLogFile string, offset int64, relayLogDir string, firstParse bool, possibleLast bool) (bool, int64, error) { 462 r.tctx.L().Debug("start to parse relay log file", zap.String("file", relayLogFile), zap.Int64("position", offset), zap.String("directory", relayLogDir)) 463 464 fullPath := filepath.Join(relayLogDir, relayLogFile) 465 f, err := os.Open(fullPath) 466 if err != nil { 467 return false, 0, errors.Trace(err) 468 } 469 defer f.Close() 470 471 state := &binlogFileParseState{ 472 possibleLast: possibleLast, 473 fullPath: fullPath, 474 relayLogFile: relayLogFile, 475 relayLogDir: relayLogDir, 476 f: f, 477 latestPos: offset, 478 skipGTID: false, 479 } 480 481 for { 482 select { 483 case <-ctx.Done(): 484 return false, 0, ctx.Err() 485 default: 486 } 487 needSwitch, needReParse, err := r.parseFile(ctx, s, firstParse, state) 488 if err != nil { 489 return false, 0, terror.Annotatef(err, "parse relay log file %s from offset %d in dir %s", relayLogFile, state.latestPos, relayLogDir) 490 } 491 firstParse = false // set to false to handle the `continue` below 492 if needReParse { 493 r.tctx.L().Debug("continue to re-parse relay log file", zap.String("file", relayLogFile), zap.String("directory", relayLogDir)) 494 continue // should continue to parse this file 495 } 496 return needSwitch, state.latestPos, nil 497 } 498 } 499 500 // parseFile parses single relay log file from specified offset. 501 func (r *BinlogReader) parseFile( 502 ctx context.Context, 503 s *LocalStreamer, 504 firstParse bool, 505 state *binlogFileParseState, 506 ) (needSwitch, needReParse bool, err error) { 507 _, suffixInt, err := utils.ParseRelaySubDir(r.currentSubDir) 508 if err != nil { 509 return false, false, err 510 } 511 512 offset := state.latestPos 513 r.lastFileGracefulEnd = false 514 515 onEventFunc := func(e *replication.BinlogEvent) error { 516 if ce := r.tctx.L().Check(zap.DebugLevel, ""); ce != nil { 517 r.tctx.L().Debug("read event", zap.Reflect("header", e.Header)) 518 } 519 r.latestServerID = e.Header.ServerID // record server_id 520 521 lastSkipGTID := state.skipGTID 522 523 switch ev := e.Event.(type) { 524 case *replication.FormatDescriptionEvent: 525 state.formatDescEventRead = true 526 state.latestPos = int64(e.Header.LogPos) 527 case *replication.RotateEvent: 528 // add master UUID suffix to pos.Name 529 parsed, _ := utils.ParseFilename(string(ev.NextLogName)) 530 uuidSuffix := utils.SuffixIntToStr(suffixInt) // current UUID's suffix, which will be added to binlog name 531 nameWithSuffix := utils.ConstructFilenameWithUUIDSuffix(parsed, uuidSuffix) 532 ev.NextLogName = []byte(nameWithSuffix) 533 534 if e.Header.Timestamp != 0 && e.Header.LogPos != 0 { 535 // not fake rotate event, update file pos 536 state.latestPos = int64(e.Header.LogPos) 537 r.lastFileGracefulEnd = true 538 } else { 539 r.tctx.L().Debug("skip fake rotate event", zap.Reflect("header", e.Header)) 540 } 541 542 // currently, we do not switch to the next relay log file when we receive the RotateEvent, 543 // because that next relay log file may not exists at this time, 544 // and we *try* to switch to the next when `needReParse` is false. 545 // so this `currentPos` only used for log now. 546 currentPos := mysql.Position{ 547 Name: string(ev.NextLogName), 548 Pos: uint32(ev.Position), 549 } 550 r.tctx.L().Info("rotate binlog", zap.Stringer("position", currentPos)) 551 case *replication.GTIDEvent, *replication.MariadbGTIDEvent: 552 if r.prevGset == nil { 553 state.latestPos = int64(e.Header.LogPos) 554 break 555 } 556 gtidStr, err2 := event.GetGTIDStr(e) 557 if err2 != nil { 558 return errors.Trace(err2) 559 } 560 state.skipGTID, err = r.advanceCurrentGtidSet(gtidStr) 561 if err != nil { 562 return errors.Trace(err) 563 } 564 state.latestPos = int64(e.Header.LogPos) 565 case *replication.XIDEvent: 566 ev.GSet = r.getCurrentGtidSet() 567 state.latestPos = int64(e.Header.LogPos) 568 case *replication.QueryEvent: 569 ev.GSet = r.getCurrentGtidSet() 570 state.latestPos = int64(e.Header.LogPos) 571 default: 572 // update file pos 573 state.latestPos = int64(e.Header.LogPos) 574 } 575 576 // align with MySQL 577 // ref https://github.com/pingcap/tiflow/issues/5063#issuecomment-1082678211 578 // heartbeat period is implemented in LocalStreamer.GetEvent 579 if state.skipGTID { 580 switch e.Event.(type) { 581 // Only replace transaction event 582 // Other events such as FormatDescriptionEvent, RotateEvent, etc. should be the same as before 583 case *replication.RowsEvent, *replication.QueryEvent, *replication.GTIDEvent, 584 *replication.MariadbGTIDEvent, *replication.XIDEvent, *replication.TableMapEvent: 585 // replace with heartbeat event 586 state.lastSkipGTIDHeader = e.Header 587 default: 588 } 589 return nil 590 } else if lastSkipGTID && state.lastSkipGTIDHeader != nil { 591 // skipGTID is turned off after this event 592 select { 593 case s.ch <- event.GenHeartbeatEvent(state.lastSkipGTIDHeader): 594 case <-ctx.Done(): 595 } 596 } 597 598 select { 599 case s.ch <- e: 600 case <-ctx.Done(): 601 } 602 return nil 603 } 604 605 if firstParse { 606 // if the file is the first time to parse, send a fake ROTATE_EVENT before parse binlog file 607 // ref: https://github.com/mysql/mysql-server/blob/4f1d7cf5fcb11a3f84cff27e37100d7295e7d5ca/sql/rpl_binlog_sender.cc#L248 608 e, err2 := utils.GenFakeRotateEvent(state.relayLogFile, uint64(offset), r.latestServerID) 609 if err2 != nil { 610 return false, false, terror.Annotatef(err2, "generate fake RotateEvent for (%s: %d)", state.relayLogFile, offset) 611 } 612 err2 = onEventFunc(e) 613 if err2 != nil { 614 return false, false, terror.Annotatef(err2, "send event %+v", e.Header) 615 } 616 r.tctx.L().Info("start parse relay log file", zap.String("file", state.fullPath), zap.Int64("offset", offset)) 617 } else { 618 r.tctx.L().Debug("start parse relay log file", zap.String("file", state.fullPath), zap.Int64("offset", offset)) 619 } 620 621 // parser needs the FormatDescriptionEvent to work correctly 622 // if we start parsing from the middle, we need to read FORMAT DESCRIPTION event first 623 if !state.formatDescEventRead && offset > binlog.FileHeaderLen { 624 if err = r.parseFormatDescEvent(state); err != nil { 625 if state.possibleLast && isIgnorableParseError(err) { 626 return r.waitBinlogChanged(ctx, state) 627 } 628 return false, false, terror.ErrParserParseRelayLog.Delegate(err, state.fullPath) 629 } 630 state.formatDescEventRead = true 631 } 632 633 // we need to seek explicitly, as parser may read in-complete event and return error(ignorable) last time 634 // and offset may be messed up 635 if _, err = state.f.Seek(offset, io.SeekStart); err != nil { 636 return false, false, terror.ErrParserParseRelayLog.Delegate(err, state.fullPath) 637 } 638 639 err = r.parser.ParseReader(state.f, onEventFunc) 640 if err != nil && (!state.possibleLast || !isIgnorableParseError(err)) { 641 r.tctx.L().Error("parse relay log file", zap.String("file", state.fullPath), zap.Int64("offset", offset), zap.Error(err)) 642 return false, false, terror.ErrParserParseRelayLog.Delegate(err, state.fullPath) 643 } 644 r.tctx.L().Debug("parse relay log file", zap.String("file", state.fullPath), zap.Int64("offset", state.latestPos)) 645 646 return r.waitBinlogChanged(ctx, state) 647 } 648 649 func (r *BinlogReader) waitBinlogChanged(ctx context.Context, state *binlogFileParseState) (needSwitch, needReParse bool, err error) { 650 active, relayOffset := r.relay.IsActive(r.currentSubDir, state.relayLogFile) 651 if active && relayOffset > state.latestPos { 652 return false, true, nil 653 } 654 if !active { 655 meta := &LocalMeta{} 656 _, err := toml.DecodeFile(filepath.Join(state.relayLogDir, utils.MetaFilename), meta) 657 if err != nil { 658 return false, false, terror.Annotate(err, "decode relay meta toml file failed") 659 } 660 // current watched file size have no change means that no new writes have been made 661 // our relay meta file will be updated immediately after receive the rotate event, 662 // although we cannot ensure that the binlog filename in the meta is the next file after latestFile 663 // but if we return a different filename with latestFile, the outer logic (parseDirAsPossible) 664 // will find the right one 665 if meta.BinLogName != state.relayLogFile { 666 // we need check file size again, as the file may have been changed during our metafile check 667 cmp, err2 := fileSizeUpdated(state.fullPath, state.latestPos) 668 if err2 != nil { 669 return false, false, terror.Annotatef(err2, "latestFilePath=%s endOffset=%d", state.fullPath, state.latestPos) 670 } 671 switch { 672 case cmp < 0: 673 return false, false, terror.ErrRelayLogFileSizeSmaller.Generate(state.fullPath) 674 case cmp > 0: 675 return false, true, nil 676 default: 677 nextFilePath := filepath.Join(state.relayLogDir, meta.BinLogName) 678 log.L().Info("newer relay log file is already generated", 679 zap.String("now file path", state.fullPath), 680 zap.String("new file path", nextFilePath)) 681 return false, false, nil 682 } 683 } 684 685 // maybe UUID index file changed 686 switchPath, err := r.getSwitchPath() 687 if err != nil { 688 return false, false, err 689 } 690 if switchPath != nil { 691 // we need check file size again, as the file may have been changed during path check 692 cmp, err := fileSizeUpdated(state.fullPath, state.latestPos) 693 if err != nil { 694 return false, false, terror.Annotatef(err, "latestFilePath=%s endOffset=%d", state.fullPath, state.latestPos) 695 } 696 switch { 697 case cmp < 0: 698 return false, false, terror.ErrRelayLogFileSizeSmaller.Generate(state.fullPath) 699 case cmp > 0: 700 return false, true, nil 701 default: 702 log.L().Info("newer relay uuid path is already generated", 703 zap.String("current path", state.relayLogDir), 704 zap.Any("new path", switchPath)) 705 return true, false, nil 706 } 707 } 708 } 709 710 for { 711 select { 712 case <-ctx.Done(): 713 return false, false, nil 714 case <-r.Notified(): 715 active, relayOffset = r.relay.IsActive(r.currentSubDir, state.relayLogFile) 716 if active { 717 if relayOffset > state.latestPos { 718 return false, true, nil 719 } 720 // already read to relayOffset, try again 721 continue 722 } 723 // file may have changed, try parse and check again 724 return false, true, nil 725 } 726 } 727 } 728 729 func (r *BinlogReader) parseFormatDescEvent(state *binlogFileParseState) error { 730 // FORMAT_DESCRIPTION event should always be read by default (despite that fact passed offset may be higher than 4) 731 if _, err := state.f.Seek(binlog.FileHeaderLen, io.SeekStart); err != nil { 732 return errors.Errorf("seek to 4, error %v", err) 733 } 734 735 onEvent := func(e *replication.BinlogEvent) error { 736 if _, ok := e.Event.(*replication.FormatDescriptionEvent); ok { 737 return nil 738 } 739 // the first event in binlog file must be FORMAT_DESCRIPTION event. 740 return errors.New("corrupted binlog file") 741 } 742 eofWhenReadHeader, err := r.parser.ParseSingleEvent(state.f, onEvent) 743 if err != nil { 744 return errors.Annotatef(err, "parse FormatDescriptionEvent") 745 } 746 if eofWhenReadHeader { 747 // when parser met EOF when reading event header, ParseSingleEvent returns nil error 748 // return EOF so isIgnorableParseError can capture 749 return io.EOF 750 } 751 return nil 752 } 753 754 // updateSubDirs re-parses UUID index file and updates subdirectory list. 755 func (r *BinlogReader) updateSubDirs() error { 756 subDirs, err := utils.ParseUUIDIndex(r.indexPath) 757 if err != nil { 758 return terror.Annotatef(err, "index file path %s", r.indexPath) 759 } 760 oldSubDirs := r.subDirs 761 r.subDirs = subDirs 762 r.tctx.L().Info("update relay UUIDs", zap.Strings("old subDirs", oldSubDirs), zap.Strings("subDirs", subDirs)) 763 return nil 764 } 765 766 // Close closes BinlogReader. 767 func (r *BinlogReader) Close() { 768 r.tctx.L().Info("binlog reader closing") 769 r.running = false 770 r.cancel() 771 r.parser.Stop() 772 r.wg.Wait() 773 r.relay.UnRegisterListener(r) 774 r.tctx.L().Info("binlog reader closed") 775 } 776 777 // GetSubDirs returns binlog reader's subDirs. 778 func (r *BinlogReader) GetSubDirs() []string { 779 ret := make([]string, 0, len(r.subDirs)) 780 ret = append(ret, r.subDirs...) 781 return ret 782 } 783 784 func (r *BinlogReader) getCurrentGtidSet() mysql.GTIDSet { 785 if r.currGset == nil { 786 return nil 787 } 788 return r.currGset.Clone() 789 } 790 791 // advanceCurrentGtidSet advance gtid set and return whether currGset not updated. 792 func (r *BinlogReader) advanceCurrentGtidSet(gtid string) (bool, error) { 793 if r.currGset == nil { 794 r.currGset = r.prevGset.Clone() 795 } 796 // Special treatment for Maridb 797 // MaridbGTIDSet.Update(gtid) will replace gset with given gtid 798 // ref https://github.com/go-mysql-org/go-mysql/blob/0c5789dd0bd378b4b84f99b320a2d35a80d8858f/mysql/mariadb_gtid.go#L96 799 if r.cfg.Flavor == mysql.MariaDBFlavor { 800 gset, err := mysql.ParseMariadbGTIDSet(gtid) 801 if err != nil { 802 return false, err 803 } 804 if r.currGset.Contain(gset) { 805 return true, nil 806 } 807 } 808 prev := r.currGset.Clone() 809 err := r.currGset.Update(gtid) 810 if err == nil { 811 if !r.currGset.Equal(prev) { 812 r.prevGset = prev 813 return false, nil 814 } 815 return true, nil 816 } 817 return false, err 818 } 819 820 func (r *BinlogReader) Notified() chan interface{} { 821 return r.notifyCh 822 } 823 824 func (r *BinlogReader) OnEvent(_ *replication.BinlogEvent) { 825 // skip if there's pending notify 826 select { 827 case r.notifyCh <- struct{}{}: 828 default: 829 } 830 }