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  }