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  }