github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/relay/relay_writer_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 "os" 19 "path" 20 "path/filepath" 21 "time" 22 23 gmysql "github.com/go-mysql-org/go-mysql/mysql" 24 "github.com/go-mysql-org/go-mysql/replication" 25 "github.com/pingcap/check" 26 "github.com/pingcap/tiflow/dm/pkg/binlog/event" 27 "github.com/pingcap/tiflow/dm/pkg/gtid" 28 "github.com/pingcap/tiflow/dm/pkg/log" 29 ) 30 31 var _ = check.Suite(&testFileWriterSuite{}) 32 33 type testFileWriterSuite struct{} 34 35 func (t *testFileWriterSuite) TestInterfaceMethods(c *check.C) { 36 var ( 37 relayDir = c.MkDir() 38 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 39 filename = "test-mysql-bin.000001" 40 header = &replication.EventHeader{ 41 Timestamp: uint32(time.Now().Unix()), 42 ServerID: 11, 43 Flags: 0x01, 44 } 45 latestPos uint32 = 4 46 ev, _ = event.GenFormatDescriptionEvent(header, latestPos) 47 ) 48 49 c.Assert(os.MkdirAll(path.Join(relayDir, uuid), 0o755), check.IsNil) 50 51 w := NewFileWriter(log.L(), relayDir) 52 c.Assert(w, check.NotNil) 53 54 // not prepared 55 _, err := w.WriteEvent(ev) 56 c.Assert(err, check.ErrorMatches, ".*not valid.*") 57 58 w.Init(uuid, filename) 59 60 // write event 61 res, err := w.WriteEvent(ev) 62 c.Assert(err, check.IsNil) 63 c.Assert(res.Ignore, check.IsFalse) 64 65 // close the writer 66 c.Assert(w.Close(), check.IsNil) 67 } 68 69 func (t *testFileWriterSuite) TestRelayDir(c *check.C) { 70 var ( 71 relayDir = c.MkDir() 72 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 73 header = &replication.EventHeader{ 74 Timestamp: uint32(time.Now().Unix()), 75 ServerID: 11, 76 Flags: 0x01, 77 } 78 latestPos uint32 = 4 79 ) 80 ev, err := event.GenFormatDescriptionEvent(header, latestPos) 81 c.Assert(err, check.IsNil) 82 83 // not inited 84 w1 := NewFileWriter(log.L(), relayDir) 85 defer w1.Close() 86 _, err = w1.WriteEvent(ev) 87 c.Assert(err, check.ErrorMatches, ".*not valid.*") 88 89 // invalid dir 90 w2 := NewFileWriter(log.L(), relayDir) 91 defer w2.Close() 92 w2.Init("invalid\x00uuid", "bin.000001") 93 _, err = w2.WriteEvent(ev) 94 c.Assert(err, check.ErrorMatches, ".*invalid argument.*") 95 96 // valid directory, but no filename specified 97 w3 := NewFileWriter(log.L(), relayDir) 98 defer w3.Close() 99 w3.Init(uuid, "") 100 _, err = w3.WriteEvent(ev) 101 c.Assert(err, check.ErrorMatches, ".*not valid.*") 102 103 // valid directory, but invalid filename 104 w4 := NewFileWriter(log.L(), relayDir) 105 defer w4.Close() 106 w4.Init(uuid, "test-mysql-bin.666abc") 107 _, err = w4.WriteEvent(ev) 108 c.Assert(err, check.ErrorMatches, ".*not valid.*") 109 110 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 111 112 // valid directory, valid filename 113 w5 := NewFileWriter(log.L(), relayDir) 114 defer w5.Close() 115 w5.Init(uuid, "test-mysql-bin.000001") 116 result, err := w5.WriteEvent(ev) 117 c.Assert(err, check.IsNil) 118 c.Assert(result.Ignore, check.IsFalse) 119 } 120 121 func (t *testFileWriterSuite) TestFormatDescriptionEvent(c *check.C) { 122 var ( 123 relayDir = c.MkDir() 124 filename = "test-mysql-bin.000001" 125 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 126 header = &replication.EventHeader{ 127 Timestamp: uint32(time.Now().Unix()), 128 ServerID: 11, 129 Flags: 0x01, 130 } 131 latestPos uint32 = 4 132 ) 133 formatDescEv, err := event.GenFormatDescriptionEvent(header, latestPos) 134 c.Assert(err, check.IsNil) 135 c.Assert(os.Mkdir(path.Join(relayDir, uuid), 0o755), check.IsNil) 136 137 // write FormatDescriptionEvent to empty file 138 w := NewFileWriter(log.L(), relayDir) 139 defer w.Close() 140 w.Init(uuid, filename) 141 result, err := w.WriteEvent(formatDescEv) 142 c.Assert(err, check.IsNil) 143 c.Assert(result.Ignore, check.IsFalse) 144 fileSize := int64(len(replication.BinLogFileHeader) + len(formatDescEv.RawData)) 145 t.verifyFilenameOffset(c, w, filename, fileSize) 146 latestPos = formatDescEv.Header.LogPos 147 148 // write FormatDescriptionEvent again, ignore 149 result, err = w.WriteEvent(formatDescEv) 150 c.Assert(err, check.IsNil) 151 c.Assert(result.Ignore, check.IsTrue) 152 c.Assert(result.IgnoreReason, check.Equals, ignoreReasonAlreadyExists) 153 t.verifyFilenameOffset(c, w, filename, fileSize) 154 155 // write another event 156 queryEv, err := event.GenQueryEvent(header, latestPos, 0, 0, 0, nil, []byte("schema"), []byte("BEGIN")) 157 c.Assert(err, check.IsNil) 158 result, err = w.WriteEvent(queryEv) 159 c.Assert(err, check.IsNil) 160 c.Assert(result.Ignore, check.IsFalse) 161 fileSize += int64(len(queryEv.RawData)) 162 t.verifyFilenameOffset(c, w, filename, fileSize) 163 164 // write FormatDescriptionEvent again, ignore 165 result, err = w.WriteEvent(formatDescEv) 166 c.Assert(err, check.IsNil) 167 c.Assert(result.Ignore, check.IsTrue) 168 c.Assert(result.IgnoreReason, check.Equals, ignoreReasonAlreadyExists) 169 t.verifyFilenameOffset(c, w, filename, fileSize) 170 171 // check events by reading them back 172 events := make([]*replication.BinlogEvent, 0, 2) 173 count := 0 174 onEventFunc := func(e *replication.BinlogEvent) error { 175 count++ 176 if count > 2 { 177 c.Fatalf("too many events received, %+v", e.Header) 178 } 179 events = append(events, e) 180 return nil 181 } 182 fullName := filepath.Join(relayDir, uuid, filename) 183 err = replication.NewBinlogParser().ParseFile(fullName, 0, onEventFunc) 184 c.Assert(err, check.IsNil) 185 c.Assert(events, check.HasLen, 2) 186 c.Assert(events[0], check.DeepEquals, formatDescEv) 187 c.Assert(events[1], check.DeepEquals, queryEv) 188 } 189 190 func (t *testFileWriterSuite) verifyFilenameOffset(c *check.C, w Writer, filename string, offset int64) { 191 wf, ok := w.(*FileWriter) 192 c.Assert(ok, check.IsTrue) 193 c.Assert(wf.filename.Load(), check.Equals, filename) 194 c.Assert(wf.offset(), check.Equals, offset) 195 } 196 197 func (t *testFileWriterSuite) TestRotateEventWithFormatDescriptionEvent(c *check.C) { 198 var ( 199 relayDir = c.MkDir() 200 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 201 filename = "test-mysql-bin.000001" 202 nextFilename = "test-mysql-bin.000002" 203 nextFilePos uint64 = 4 204 header = &replication.EventHeader{ 205 Timestamp: uint32(time.Now().Unix()), 206 ServerID: 11, 207 Flags: 0x01, 208 } 209 fakeHeader = &replication.EventHeader{ 210 Timestamp: 0, // mark as fake 211 ServerID: 11, 212 Flags: 0x01, 213 } 214 latestPos uint32 = 4 215 ) 216 217 formatDescEv, err := event.GenFormatDescriptionEvent(header, latestPos) 218 c.Assert(err, check.IsNil) 219 c.Assert(formatDescEv, check.NotNil) 220 latestPos = formatDescEv.Header.LogPos 221 222 rotateEv, err := event.GenRotateEvent(header, latestPos, []byte(nextFilename), nextFilePos) 223 c.Assert(err, check.IsNil) 224 c.Assert(rotateEv, check.NotNil) 225 226 fakeRotateEv, err := event.GenRotateEvent(fakeHeader, latestPos, []byte(nextFilename), nextFilePos) 227 c.Assert(err, check.IsNil) 228 c.Assert(fakeRotateEv, check.NotNil) 229 230 // hole exists between formatDescEv and holeRotateEv, but the size is too small to fill 231 holeRotateEv, err := event.GenRotateEvent(header, latestPos+event.MinUserVarEventLen-1, []byte(nextFilename), nextFilePos) 232 c.Assert(err, check.IsNil) 233 c.Assert(holeRotateEv, check.NotNil) 234 235 // 1: non-fake RotateEvent before FormatDescriptionEvent, invalid 236 w1 := NewFileWriter(log.L(), relayDir) 237 defer w1.Close() 238 w1.Init(uuid, filename) 239 _, err = w1.WriteEvent(rotateEv) 240 c.Assert(err, check.ErrorMatches, ".*no underlying writer opened") 241 242 // 2. fake RotateEvent before FormatDescriptionEvent 243 relayDir = c.MkDir() // use a new relay directory 244 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 245 w2 := NewFileWriter(log.L(), relayDir) 246 defer w2.Close() 247 w2.Init(uuid, filename) 248 result, err := w2.WriteEvent(fakeRotateEv) 249 c.Assert(err, check.IsNil) 250 c.Assert(result.Ignore, check.IsTrue) // ignore fake RotateEvent 251 c.Assert(result.IgnoreReason, check.Equals, ignoreReasonFakeRotate) 252 253 result, err = w2.WriteEvent(formatDescEv) 254 c.Assert(err, check.IsNil) 255 c.Assert(result.Ignore, check.IsFalse) 256 257 fileSize := int64(len(replication.BinLogFileHeader) + len(formatDescEv.RawData)) 258 t.verifyFilenameOffset(c, w2, nextFilename, fileSize) 259 260 // filename should be empty, next file should contain only one FormatDescriptionEvent 261 filename1 := filepath.Join(relayDir, uuid, filename) 262 filename2 := filepath.Join(relayDir, uuid, nextFilename) 263 _, err = os.Stat(filename1) 264 c.Assert(os.IsNotExist(err), check.IsTrue) 265 c.Assert(w2.Flush(), check.IsNil) 266 data, err := os.ReadFile(filename2) 267 c.Assert(err, check.IsNil) 268 fileHeaderLen := len(replication.BinLogFileHeader) 269 c.Assert(len(data), check.Equals, fileHeaderLen+len(formatDescEv.RawData)) 270 c.Assert(data[fileHeaderLen:], check.DeepEquals, formatDescEv.RawData) 271 272 // 3. FormatDescriptionEvent before fake RotateEvent 273 relayDir = c.MkDir() // use a new relay directory 274 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 275 w3 := NewFileWriter(log.L(), relayDir) 276 defer w3.Close() 277 w3.Init(uuid, filename) 278 result, err = w3.WriteEvent(formatDescEv) 279 c.Assert(err, check.IsNil) 280 c.Assert(result, check.NotNil) 281 c.Assert(result.Ignore, check.IsFalse) 282 283 result, err = w3.WriteEvent(fakeRotateEv) 284 c.Assert(err, check.IsNil) 285 c.Assert(result, check.NotNil) 286 c.Assert(result.Ignore, check.IsTrue) 287 c.Assert(result.IgnoreReason, check.Equals, ignoreReasonFakeRotate) 288 289 t.verifyFilenameOffset(c, w3, nextFilename, fileSize) 290 291 // filename should contain only one FormatDescriptionEvent, next file should be empty 292 filename1 = filepath.Join(relayDir, uuid, filename) 293 filename2 = filepath.Join(relayDir, uuid, nextFilename) 294 _, err = os.Stat(filename2) 295 c.Assert(os.IsNotExist(err), check.IsTrue) 296 c.Assert(w3.Flush(), check.IsNil) 297 data, err = os.ReadFile(filename1) 298 c.Assert(err, check.IsNil) 299 c.Assert(len(data), check.Equals, fileHeaderLen+len(formatDescEv.RawData)) 300 c.Assert(data[fileHeaderLen:], check.DeepEquals, formatDescEv.RawData) 301 302 // 4. FormatDescriptionEvent before non-fake RotateEvent 303 relayDir = c.MkDir() // use a new relay directory 304 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 305 w4 := NewFileWriter(log.L(), relayDir) 306 defer w4.Close() 307 w4.Init(uuid, filename) 308 result, err = w4.WriteEvent(formatDescEv) 309 c.Assert(err, check.IsNil) 310 c.Assert(result, check.NotNil) 311 c.Assert(result.Ignore, check.IsFalse) 312 313 // try to write a rotateEv with hole exists 314 _, err = w4.WriteEvent(holeRotateEv) 315 c.Assert(err, check.ErrorMatches, ".*required dummy event size.*is too small.*") 316 317 result, err = w4.WriteEvent(rotateEv) 318 c.Assert(err, check.IsNil) 319 c.Assert(result.Ignore, check.IsFalse) 320 321 fileSize += int64(len(rotateEv.RawData)) 322 t.verifyFilenameOffset(c, w4, nextFilename, fileSize) 323 324 // write again, duplicate, but we already rotated and new binlog file not created 325 _, err = w4.WriteEvent(rotateEv) 326 c.Assert(err, check.ErrorMatches, ".*(no such file or directory|The system cannot find the file specified).*") 327 328 // filename should contain both one FormatDescriptionEvent and one RotateEvent, next file should be empty 329 filename1 = filepath.Join(relayDir, uuid, filename) 330 filename2 = filepath.Join(relayDir, uuid, nextFilename) 331 _, err = os.Stat(filename2) 332 c.Assert(os.IsNotExist(err), check.IsTrue) 333 data, err = os.ReadFile(filename1) 334 c.Assert(err, check.IsNil) 335 c.Assert(len(data), check.Equals, fileHeaderLen+len(formatDescEv.RawData)+len(rotateEv.RawData)) 336 c.Assert(data[fileHeaderLen:fileHeaderLen+len(formatDescEv.RawData)], check.DeepEquals, formatDescEv.RawData) 337 c.Assert(data[fileHeaderLen+len(formatDescEv.RawData):], check.DeepEquals, rotateEv.RawData) 338 } 339 340 func (t *testFileWriterSuite) TestWriteMultiEvents(c *check.C) { 341 var ( 342 flavor = gmysql.MySQLFlavor 343 serverID uint32 = 11 344 latestPos uint32 345 previousGTIDSetStr = "3ccc475b-2343-11e7-be21-6c0b84d59f30:1-14,406a3f61-690d-11e7-87c5-6c92bf46f384:1-94321383,53bfca22-690d-11e7-8a62-18ded7a37b78:1-495,686e1ab6-c47e-11e7-a42c-6c92bf46f384:1-34981190,03fc0263-28c7-11e7-a653-6c0b84d59f30:1-7041423,05474d3c-28c7-11e7-8352-203db246dd3d:1-170,10b039fc-c843-11e7-8f6a-1866daf8d810:1-308290454" 346 latestGTIDStr = "3ccc475b-2343-11e7-be21-6c0b84d59f30:14" 347 latestXID uint64 = 10 348 349 relayDir = c.MkDir() 350 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 351 filename = "test-mysql-bin.000001" 352 ) 353 previousGTIDSet, err := gtid.ParserGTID(flavor, previousGTIDSetStr) 354 c.Assert(err, check.IsNil) 355 latestGTID, err := gtid.ParserGTID(flavor, latestGTIDStr) 356 c.Assert(err, check.IsNil) 357 358 // use a binlog event generator to generate some binlog events. 359 allEvents := make([]*replication.BinlogEvent, 0, 10) 360 var allData bytes.Buffer 361 g, err := event.NewGenerator(flavor, serverID, latestPos, latestGTID, previousGTIDSet, latestXID) 362 c.Assert(err, check.IsNil) 363 364 // file header with FormatDescriptionEvent and PreviousGTIDsEvent 365 events, data, err := g.GenFileHeader(0) 366 c.Assert(err, check.IsNil) 367 allEvents = append(allEvents, events...) 368 allData.Write(data) 369 370 // CREATE DATABASE/TABLE 371 queries := []string{"CRATE DATABASE `db`", "CREATE TABLE `db`.`tbl` (c1 INT)"} 372 for _, query := range queries { 373 events, data, err = g.GenDDLEvents("db", query, 0) 374 c.Assert(err, check.IsNil) 375 allEvents = append(allEvents, events...) 376 allData.Write(data) 377 } 378 379 // INSERT INTO `db`.`tbl` VALUES (1) 380 var ( 381 tableID uint64 = 8 382 columnType = []byte{gmysql.MYSQL_TYPE_LONG} 383 insertRows = make([][]interface{}, 1) 384 ) 385 insertRows[0] = []interface{}{int32(1)} 386 events, data, err = g.GenDMLEvents(replication.WRITE_ROWS_EVENTv2, []*event.DMLData{ 387 {TableID: tableID, Schema: "db", Table: "tbl", ColumnType: columnType, Rows: insertRows}, 388 }, 0) 389 c.Assert(err, check.IsNil) 390 allEvents = append(allEvents, events...) 391 allData.Write(data) 392 393 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 394 395 // write the events to the file 396 w := NewFileWriter(log.L(), relayDir) 397 w.Init(uuid, filename) 398 for _, ev := range allEvents { 399 result, err2 := w.WriteEvent(ev) 400 c.Assert(err2, check.IsNil) 401 c.Assert(result.Ignore, check.IsFalse) // no event is ignored 402 } 403 404 c.Assert(w.Flush(), check.IsNil) 405 t.verifyFilenameOffset(c, w, filename, int64(allData.Len())) 406 407 // read the data back from the file 408 fullName := filepath.Join(relayDir, uuid, filename) 409 obtainData, err := os.ReadFile(fullName) 410 c.Assert(err, check.IsNil) 411 c.Assert(obtainData, check.DeepEquals, allData.Bytes()) 412 } 413 414 func (t *testFileWriterSuite) TestHandleFileHoleExist(c *check.C) { 415 var ( 416 relayDir = c.MkDir() 417 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 418 filename = "test-mysql-bin.000001" 419 header = &replication.EventHeader{ 420 Timestamp: uint32(time.Now().Unix()), 421 ServerID: 11, 422 } 423 latestPos uint32 = 4 424 ) 425 formatDescEv, err := event.GenFormatDescriptionEvent(header, latestPos) 426 c.Assert(err, check.IsNil) 427 c.Assert(formatDescEv, check.NotNil) 428 429 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 430 431 w := NewFileWriter(log.L(), relayDir) 432 defer w.Close() 433 w.Init(uuid, filename) 434 435 // write the FormatDescriptionEvent, no hole exists 436 result, err := w.WriteEvent(formatDescEv) 437 c.Assert(err, check.IsNil) 438 c.Assert(result.Ignore, check.IsFalse) 439 440 // hole exits, but the size is too small, invalid 441 latestPos = formatDescEv.Header.LogPos + event.MinUserVarEventLen - 1 442 queryEv, err := event.GenQueryEvent(header, latestPos, 0, 0, 0, nil, []byte("schema"), []byte("BEGIN")) 443 c.Assert(err, check.IsNil) 444 _, err = w.WriteEvent(queryEv) 445 c.Assert(err, check.ErrorMatches, ".*generate dummy event.*") 446 447 // hole exits, and the size is enough 448 latestPos = formatDescEv.Header.LogPos + event.MinUserVarEventLen 449 queryEv, err = event.GenQueryEvent(header, latestPos, 0, 0, 0, nil, []byte("schema"), []byte("BEGIN")) 450 c.Assert(err, check.IsNil) 451 result, err = w.WriteEvent(queryEv) 452 c.Assert(err, check.IsNil) 453 c.Assert(result.Ignore, check.IsFalse) 454 c.Assert(w.Flush(), check.IsNil) 455 fileSize := int64(queryEv.Header.LogPos) 456 t.verifyFilenameOffset(c, w, filename, fileSize) 457 458 // read events back from the file to check the dummy event 459 events := make([]*replication.BinlogEvent, 0, 3) 460 count := 0 461 onEventFunc := func(e *replication.BinlogEvent) error { 462 count++ 463 if count > 3 { 464 c.Fatalf("too many events received, %+v", e.Header) 465 } 466 events = append(events, e) 467 return nil 468 } 469 fullName := filepath.Join(relayDir, uuid, filename) 470 err = replication.NewBinlogParser().ParseFile(fullName, 0, onEventFunc) 471 c.Assert(err, check.IsNil) 472 c.Assert(events, check.HasLen, 3) 473 c.Assert(events[0], check.DeepEquals, formatDescEv) 474 c.Assert(events[2], check.DeepEquals, queryEv) 475 // the second event is the dummy event 476 dummyEvent := events[1] 477 c.Assert(dummyEvent.Header.EventType, check.Equals, replication.USER_VAR_EVENT) 478 c.Assert(dummyEvent.Header.LogPos, check.Equals, latestPos) // start pos of the third event 479 c.Assert(dummyEvent.Header.EventSize, check.Equals, latestPos-formatDescEv.Header.LogPos) // hole size 480 } 481 482 func (t *testFileWriterSuite) TestHandleDuplicateEventsExist(c *check.C) { 483 // NOTE: not duplicate event already tested in other cases 484 485 var ( 486 relayDir = c.MkDir() 487 uuid = "3ccc475b-2343-11e7-be21-6c0b84d59f30.000001" 488 filename = "test-mysql-bin.000001" 489 header = &replication.EventHeader{ 490 Timestamp: uint32(time.Now().Unix()), 491 ServerID: 11, 492 } 493 latestPos uint32 = 4 494 ) 495 c.Assert(os.MkdirAll(filepath.Join(relayDir, uuid), 0o755), check.IsNil) 496 w := NewFileWriter(log.L(), relayDir) 497 defer w.Close() 498 w.Init(uuid, filename) 499 500 // write a FormatDescriptionEvent, not duplicate 501 formatDescEv, err := event.GenFormatDescriptionEvent(header, latestPos) 502 c.Assert(err, check.IsNil) 503 result, err := w.WriteEvent(formatDescEv) 504 c.Assert(err, check.IsNil) 505 c.Assert(result.Ignore, check.IsFalse) 506 latestPos = formatDescEv.Header.LogPos 507 508 // write a QueryEvent, the first time, not duplicate 509 queryEv, err := event.GenQueryEvent(header, latestPos, 0, 0, 0, nil, []byte("schema"), []byte("BEGIN")) 510 c.Assert(err, check.IsNil) 511 result, err = w.WriteEvent(queryEv) 512 c.Assert(err, check.IsNil) 513 c.Assert(result.Ignore, check.IsFalse) 514 515 // write the QueryEvent again, duplicate 516 result, err = w.WriteEvent(queryEv) 517 c.Assert(err, check.IsNil) 518 c.Assert(result.Ignore, check.IsTrue) 519 c.Assert(result.IgnoreReason, check.Equals, ignoreReasonAlreadyExists) 520 521 // write a start/end pos mismatched event 522 latestPos-- 523 queryEv, err = event.GenQueryEvent(header, latestPos, 0, 0, 0, nil, []byte("schema"), []byte("BEGIN")) 524 c.Assert(err, check.IsNil) 525 _, err = w.WriteEvent(queryEv) 526 c.Assert(err, check.ErrorMatches, ".*handle a potential duplicate event.*") 527 }