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 }