github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/binlog/position.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 binlog
    15  
    16  import (
    17  	"fmt"
    18  	"strconv"
    19  	"strings"
    20  
    21  	gmysql "github.com/go-mysql-org/go-mysql/mysql"
    22  	"github.com/pingcap/tiflow/dm/pkg/gtid"
    23  	"github.com/pingcap/tiflow/dm/pkg/log"
    24  	"github.com/pingcap/tiflow/dm/pkg/terror"
    25  	"github.com/pingcap/tiflow/dm/pkg/utils"
    26  	"go.uber.org/zap"
    27  )
    28  
    29  const (
    30  	// in order to differ binlog position from multiple (switched) masters, we added a suffix which comes from relay log
    31  	// subdirectory into binlogPos.Name. And we also need support position with RelaySubDirSuffix should always > position
    32  	// without RelaySubDirSuffix, so we can continue from latter to former automatically.
    33  	// convertedPos.BinlogName =
    34  	//   originalPos.BinlogBaseName + posRelaySubDirSuffixSeparator + RelaySubDirSuffix + binlogFilenameSep + originalPos.BinlogSeq
    35  	// eg. mysql-bin.000003 under folder c6ae5afe-c7a3-11e8-a19d-0242ac130006.000002 => mysql-bin|000002.000003
    36  	// when new relay log subdirectory is created, RelaySubDirSuffix should increase.
    37  	posRelaySubDirSuffixSeparator = utils.PosRelaySubDirSuffixSeparator
    38  	// MinRelaySubDirSuffix is same as relay.MinRelaySubDirSuffix.
    39  	MinRelaySubDirSuffix = 1
    40  	// FileHeaderLen is the length of binlog file header.
    41  	FileHeaderLen = 4
    42  )
    43  
    44  // MinPosition is the min binlog position.
    45  var MinPosition = gmysql.Position{Pos: 4}
    46  
    47  // PositionFromStr constructs a mysql.Position from a string representation like `mysql-bin.000001:2345`.
    48  func PositionFromStr(s string) (gmysql.Position, error) {
    49  	parsed := strings.Split(s, ":")
    50  	if len(parsed) != 2 {
    51  		return gmysql.Position{}, terror.ErrBinlogParsePosFromStr.Generatef("the format should be filename:pos, position string %s", s)
    52  	}
    53  	pos, err := strconv.ParseUint(parsed[1], 10, 32)
    54  	if err != nil {
    55  		return gmysql.Position{}, terror.ErrBinlogParsePosFromStr.Generatef("the pos should be digital, position string %s", s)
    56  	}
    57  
    58  	return gmysql.Position{
    59  		Name: parsed[0],
    60  		Pos:  uint32(pos),
    61  	}, nil
    62  }
    63  
    64  func trimBrackets(s string) string {
    65  	if len(s) > 2 && s[0] == '(' && s[len(s)-1] == ')' {
    66  		return s[1 : len(s)-1]
    67  	}
    68  	return s
    69  }
    70  
    71  // PositionFromPosStr constructs a mysql.Position from a string representation like `(mysql-bin.000001, 2345)`.
    72  func PositionFromPosStr(str string) (gmysql.Position, error) {
    73  	s := trimBrackets(str)
    74  	parsed := strings.Split(s, ", ")
    75  	if len(parsed) != 2 {
    76  		return gmysql.Position{}, terror.ErrBinlogParsePosFromStr.Generatef("invalid binlog pos, should be like (mysql-bin.000001, 2345), got %s", str)
    77  	}
    78  	pos, err := strconv.ParseUint(parsed[1], 10, 32)
    79  	if err != nil {
    80  		return gmysql.Position{}, terror.ErrBinlogParsePosFromStr.Generatef("the pos should be digital, position string %s", str)
    81  	}
    82  
    83  	return gmysql.Position{
    84  		Name: parsed[0],
    85  		Pos:  uint32(pos),
    86  	}, nil
    87  }
    88  
    89  // RealMySQLPos parses a relay position and returns a mysql position and whether error occurs
    90  // if parsed successfully and `RelaySubDirSuffix` in binlog filename exists, sets position Name to
    91  // `originalPos.BinlogBaseName + binlogFilenameSep + originalPos.BinlogSeq`.
    92  // if parsed failed returns the given position and the traced error.
    93  func RealMySQLPos(pos gmysql.Position) (gmysql.Position, error) {
    94  	parsed, err := utils.ParseFilename(pos.Name)
    95  	if err != nil {
    96  		return pos, err
    97  	}
    98  
    99  	sepIdx := strings.LastIndex(parsed.BaseName, posRelaySubDirSuffixSeparator)
   100  	if sepIdx > 0 && sepIdx+len(posRelaySubDirSuffixSeparator) < len(parsed.BaseName) {
   101  		if !verifyRelaySubDirSuffix(parsed.BaseName[sepIdx+len(posRelaySubDirSuffixSeparator):]) {
   102  			// NOTE: still can't handle the case where `log-bin` has the format of `mysql-bin|666888`.
   103  			return pos, nil // pos is just the real pos
   104  		}
   105  		return gmysql.Position{
   106  			Name: utils.ConstructFilename(parsed.BaseName[:sepIdx], parsed.Seq),
   107  			Pos:  pos.Pos,
   108  		}, nil
   109  	}
   110  
   111  	return pos, nil
   112  }
   113  
   114  // ExtractSuffix extracts RelaySubDirSuffix from input name.
   115  func ExtractSuffix(name string) (int, error) {
   116  	if len(name) == 0 {
   117  		return MinRelaySubDirSuffix, nil
   118  	}
   119  	filename, err := utils.ParseFilename(name)
   120  	if err != nil {
   121  		return 0, err
   122  	}
   123  	sepIdx := strings.LastIndex(filename.BaseName, posRelaySubDirSuffixSeparator)
   124  	if sepIdx > 0 && sepIdx+len(posRelaySubDirSuffixSeparator) < len(filename.BaseName) {
   125  		suffix := filename.BaseName[sepIdx+len(posRelaySubDirSuffixSeparator):]
   126  		v, err := strconv.ParseInt(suffix, 10, 64)
   127  		return int(v), err
   128  	}
   129  	return MinRelaySubDirSuffix, nil
   130  }
   131  
   132  // ExtractPos extracts (uuidWithSuffix, RelaySubDirSuffix, originalPos) from input position (originalPos or convertedPos).
   133  // nolint:nakedret
   134  func ExtractPos(pos gmysql.Position, uuids []string) (uuidWithSuffix string, relaySubDirSuffix string, realPos gmysql.Position, err error) {
   135  	if len(uuids) == 0 {
   136  		err = terror.ErrBinlogExtractPosition.New("empty UUIDs not valid")
   137  		return
   138  	}
   139  
   140  	parsed, err := utils.ParseFilename(pos.Name)
   141  	if err != nil {
   142  		return
   143  	}
   144  	sepIdx := strings.LastIndex(parsed.BaseName, posRelaySubDirSuffixSeparator)
   145  	if sepIdx > 0 && sepIdx+len(posRelaySubDirSuffixSeparator) < len(parsed.BaseName) {
   146  		realBaseName, masterRelaySubDirSuffix := parsed.BaseName[:sepIdx], parsed.BaseName[sepIdx+len(posRelaySubDirSuffixSeparator):]
   147  		if !verifyRelaySubDirSuffix(masterRelaySubDirSuffix) {
   148  			err = terror.ErrBinlogExtractPosition.Generatef("invalid UUID suffix %s", masterRelaySubDirSuffix)
   149  			return
   150  		}
   151  
   152  		// NOTE: still can't handle the case where `log-bin` has the format of `mysql-bin|666888` and UUID suffix `666888` exists.
   153  		uuid := utils.GetUUIDBySuffix(uuids, masterRelaySubDirSuffix)
   154  
   155  		if len(uuid) > 0 {
   156  			// valid UUID found
   157  			uuidWithSuffix = uuid
   158  			relaySubDirSuffix = masterRelaySubDirSuffix
   159  			realPos = gmysql.Position{
   160  				Name: utils.ConstructFilename(realBaseName, parsed.Seq),
   161  				Pos:  pos.Pos,
   162  			}
   163  		} else {
   164  			err = terror.ErrBinlogExtractPosition.Generatef("UUID suffix %s with UUIDs %v not found", masterRelaySubDirSuffix, uuids)
   165  		}
   166  		return
   167  	}
   168  
   169  	// use the latest
   170  	var suffixInt int
   171  	uuid := uuids[len(uuids)-1]
   172  	_, suffixInt, err = utils.ParseRelaySubDir(uuid)
   173  	if err != nil {
   174  		return
   175  	}
   176  	uuidWithSuffix = uuid
   177  	relaySubDirSuffix = utils.SuffixIntToStr(suffixInt)
   178  	realPos = pos // pos is realPos
   179  	return
   180  }
   181  
   182  // verifyRelaySubDirSuffix verifies suffix whether is a valid relay log subdirectory suffix.
   183  func verifyRelaySubDirSuffix(suffix string) bool {
   184  	v, err := strconv.ParseInt(suffix, 10, 64)
   185  	if err != nil || v <= 0 {
   186  		return false
   187  	}
   188  	return true
   189  }
   190  
   191  // RemoveRelaySubDirSuffix removes relay dir suffix from binlog filename of a position.
   192  // for example: mysql-bin|000001.000002 -> mysql-bin.000002.
   193  func RemoveRelaySubDirSuffix(pos gmysql.Position) gmysql.Position {
   194  	realPos, err := RealMySQLPos(pos)
   195  	if err != nil {
   196  		// just return the origin pos
   197  		return pos
   198  	}
   199  
   200  	return realPos
   201  }
   202  
   203  // VerifyBinlogPos verify binlog pos string.
   204  func VerifyBinlogPos(pos string) (*gmysql.Position, error) {
   205  	binlogPosStr := utils.TrimQuoteMark(pos)
   206  	pos2, err := PositionFromStr(binlogPosStr)
   207  	if err != nil {
   208  		return nil, terror.ErrVerifyHandleErrorArgs.Generatef("invalid --binlog-pos %s in handle-error operation: %s", binlogPosStr, terror.Message(err))
   209  	}
   210  	return &pos2, nil
   211  }
   212  
   213  // ComparePosition returns:
   214  //
   215  //	1 if pos1 is bigger than pos2
   216  //	0 if pos1 is equal to pos2
   217  //	-1 if pos1 is less than pos2
   218  func ComparePosition(pos1, pos2 gmysql.Position) int {
   219  	adjustedPos1 := RemoveRelaySubDirSuffix(pos1)
   220  	adjustedPos2 := RemoveRelaySubDirSuffix(pos2)
   221  
   222  	// means both pos1 and pos2 have uuid in name, so need also compare the uuid
   223  	if adjustedPos1.Name != pos1.Name && adjustedPos2.Name != pos2.Name {
   224  		return pos1.Compare(pos2)
   225  	}
   226  
   227  	return adjustedPos1.Compare(adjustedPos2)
   228  }
   229  
   230  // Location identifies the location of binlog events.
   231  type Location struct {
   232  	// a structure represents the file offset in binlog file
   233  	Position gmysql.Position
   234  	// executed GTID set at this location.
   235  	gtidSet gmysql.GTIDSet
   236  	// used to distinguish injected events by DM when it's not 0
   237  	Suffix int
   238  }
   239  
   240  // ZeroLocation returns a new Location. The flavor should not be empty.
   241  func ZeroLocation(flavor string) (Location, error) {
   242  	gset, err := gtid.ZeroGTIDSet(flavor)
   243  	if err != nil {
   244  		return Location{}, err
   245  	}
   246  	return Location{
   247  		Position: MinPosition,
   248  		gtidSet:  gset,
   249  	}, nil
   250  }
   251  
   252  // MustZeroLocation returns a new Location. The flavor must not be empty.
   253  // in DM the flavor is adjusted before write to etcd.
   254  func MustZeroLocation(flavor string) Location {
   255  	return Location{
   256  		Position: MinPosition,
   257  		gtidSet:  gtid.MustZeroGTIDSet(flavor),
   258  	}
   259  }
   260  
   261  // NewLocation creates a new Location from given binlog position and GTID.
   262  func NewLocation(pos gmysql.Position, gset gmysql.GTIDSet) Location {
   263  	return Location{
   264  		Position: pos,
   265  		gtidSet:  gset,
   266  	}
   267  }
   268  
   269  func (l Location) String() string {
   270  	if l.Suffix == 0 {
   271  		return fmt.Sprintf("position: %v, gtid-set: %s", l.Position, l.GTIDSetStr())
   272  	}
   273  	return fmt.Sprintf("position: %v, gtid-set: %s, suffix: %d", l.Position, l.GTIDSetStr(), l.Suffix)
   274  }
   275  
   276  // GTIDSetStr returns gtid set's string.
   277  func (l Location) GTIDSetStr() string {
   278  	gsetStr := ""
   279  	if l.gtidSet != nil {
   280  		gsetStr = l.gtidSet.String()
   281  	}
   282  
   283  	return gsetStr
   284  }
   285  
   286  // Clone clones a same Location.
   287  func (l Location) Clone() Location {
   288  	return l.CloneWithFlavor("")
   289  }
   290  
   291  // CloneWithFlavor clones the location, and if the GTIDSet is nil, will create a GTIDSet with specified flavor.
   292  func (l Location) CloneWithFlavor(flavor string) Location {
   293  	var newGTIDSet gmysql.GTIDSet
   294  	if l.gtidSet != nil {
   295  		newGTIDSet = l.gtidSet.Clone()
   296  	} else if len(flavor) != 0 {
   297  		newGTIDSet = gtid.MustZeroGTIDSet(flavor)
   298  	}
   299  
   300  	return Location{
   301  		Position: l.Position,
   302  		gtidSet:  newGTIDSet,
   303  		Suffix:   l.Suffix,
   304  	}
   305  }
   306  
   307  // CompareLocation returns:
   308  //
   309  //	1 if point1 is bigger than point2
   310  //	0 if point1 is equal to point2
   311  //	-1 if point1 is less than point2
   312  func CompareLocation(location1, location2 Location, cmpGTID bool) int {
   313  	if cmpGTID {
   314  		cmp, canCmp := CompareGTID(location1.gtidSet, location2.gtidSet)
   315  		if canCmp {
   316  			if cmp != 0 {
   317  				return cmp
   318  			}
   319  			return compareInjectSuffix(location1.Suffix, location2.Suffix)
   320  		}
   321  
   322  		// if can't compare by GTIDSet, then compare by position
   323  		log.L().Warn("gtidSet can't be compared, will compare by position", zap.Stringer("location1", location1), zap.Stringer("location2", location2))
   324  	}
   325  
   326  	cmp := ComparePosition(location1.Position, location2.Position)
   327  	if cmp != 0 {
   328  		return cmp
   329  	}
   330  	return compareInjectSuffix(location1.Suffix, location2.Suffix)
   331  }
   332  
   333  // IsFreshPosition returns true when location1 is a fresh location without any info.
   334  func IsFreshPosition(location Location, flavor string, cmpGTID bool) bool {
   335  	zeroLocation := MustZeroLocation(flavor)
   336  	if cmpGTID {
   337  		cmp, canCmp := CompareGTID(location.gtidSet, zeroLocation.gtidSet)
   338  		if canCmp {
   339  			switch {
   340  			case cmp > 0:
   341  				return false
   342  			case cmp < 0:
   343  				// should not happen
   344  				return true
   345  			}
   346  			// empty GTIDSet, then compare by position
   347  			log.L().Warn("given gtidSets is empty, will compare by position", zap.Stringer("location", location))
   348  		} else {
   349  			// if can't compare by GTIDSet, then compare by position
   350  			log.L().Warn("gtidSet can't be compared, will compare by position", zap.Stringer("location", location))
   351  		}
   352  	}
   353  
   354  	cmp := ComparePosition(location.Position, zeroLocation.Position)
   355  	if cmp != 0 {
   356  		return cmp <= 0
   357  	}
   358  	return compareInjectSuffix(location.Suffix, zeroLocation.Suffix) <= 0
   359  }
   360  
   361  // CompareGTID returns:
   362  //
   363  //	1, true if gSet1 is bigger than gSet2
   364  //	0, true if gSet1 is equal to gSet2
   365  //	-1, true if gSet1 is less than gSet2
   366  //
   367  // but if can't compare gSet1 and gSet2, will returns 0, false.
   368  func CompareGTID(gSet1, gSet2 gmysql.GTIDSet) (int, bool) {
   369  	gSetIsEmpty1 := gtid.CheckGTIDSetEmpty(gSet1)
   370  	gSetIsEmpty2 := gtid.CheckGTIDSetEmpty(gSet2)
   371  
   372  	switch {
   373  	case gSetIsEmpty1 && gSetIsEmpty2:
   374  		// both gSet1 and gSet2 is nil
   375  		return 0, true
   376  	case gSetIsEmpty1:
   377  		return -1, true
   378  	case gSetIsEmpty2:
   379  		return 1, true
   380  	}
   381  
   382  	// both gSet1 and gSet2 is not nil
   383  	contain1 := gSet1.Contain(gSet2)
   384  	contain2 := gSet2.Contain(gSet1)
   385  	if contain1 && contain2 {
   386  		// gtidSet1 contains gtidSet2 and gtidSet2 contains gtidSet1 means gtidSet1 equals to gtidSet2,
   387  		return 0, true
   388  	}
   389  
   390  	if contain1 {
   391  		return 1, true
   392  	} else if contain2 {
   393  		return -1, true
   394  	}
   395  
   396  	return 0, false
   397  }
   398  
   399  func compareInjectSuffix(lhs, rhs int) int {
   400  	switch {
   401  	case lhs < rhs:
   402  		return -1
   403  	case lhs > rhs:
   404  		return 1
   405  	default:
   406  		return 0
   407  	}
   408  }
   409  
   410  // ResetSuffix set suffix to 0.
   411  func (l *Location) ResetSuffix() {
   412  	l.Suffix = 0
   413  }
   414  
   415  // CopyWithoutSuffixFrom copies a same Location without suffix. Note that gtidSet is shared.
   416  func (l *Location) CopyWithoutSuffixFrom(from Location) {
   417  	l.Position = from.Position
   418  	l.gtidSet = from.gtidSet
   419  }
   420  
   421  // SetGTID set new gtid for location.
   422  // TODO: don't change old Location and return a new one to copy-on-write.
   423  func (l *Location) SetGTID(gset gmysql.GTIDSet) error {
   424  	l.gtidSet = gset
   425  	return nil
   426  }
   427  
   428  // GetGTID return gtidSet of Location.
   429  // NOTE: for most cases you should clone before call Update on the returned GTID
   430  // set, unless you know there's no other reference using the GTID set.
   431  func (l *Location) GetGTID() gmysql.GTIDSet {
   432  	return l.gtidSet
   433  }
   434  
   435  // Update will update GTIDSet of Location.
   436  // caller should be aware that this will change the GTID set of other copies.
   437  // TODO: don't change old Location and return a new one to copy-on-write.
   438  func (l *Location) Update(gtidStr string) error {
   439  	return l.gtidSet.Update(gtidStr)
   440  }