Skip to content

Commit fdd4673

Browse files
committed
fix(mysqlcommon): flush binlog event at marker
- Flush a completed row event at each "# at" marker instead of waiting for the next ### op-line or EOF. In an unbounded CDC stream the next op-line may never arrive during a quiet period, so the last change would otherwise stay buffered and never reach the target. - The marker is the existing event boundary already used for the bounded stopAt check; flushing there is safe and does not double-emit (flush nils the pending event). - Add a regression test asserting events flush at the following marker.
1 parent dc1b49f commit fdd4673

2 files changed

Lines changed: 54 additions & 7 deletions

File tree

internal/driver/_mysqlcommon/changestream.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,18 @@ func parseBinlogRows(r io.Reader, meta *tableMetaCache, emit func(canonical.Cano
179179
// Track the current binlog offset from "# at <pos>" markers.
180180
if p, ok := parseAtMarker(line); ok {
181181
pos.Position = p
182-
// Bounded (incremental) stop: an "# at" marker precedes each event, so
183-
// once the marker reaches/passes the captured end position in the end
184-
// file, every earlier event has been parsed. Flush the pending event
185-
// and return at this clean boundary.
182+
// An "# at" marker precedes each event, so it is the boundary at which
183+
// the preceding event is complete. Flush the pending event here so it
184+
// is emitted promptly — without this, in an unbounded CDC stream the
185+
// last change before a quiet period would sit buffered until the next
186+
// event (or EOF, which never comes), never reaching the target.
187+
if err := flush(); err != nil {
188+
return pos, err
189+
}
190+
// Bounded (incremental) stop: once the marker reaches/passes the
191+
// captured end position in the end file, every earlier event has been
192+
// parsed and flushed above, so return at this clean boundary.
186193
if stopAt != nil && pos.File == stopAt.File && pos.Position >= stopAt.Position {
187-
if err := flush(); err != nil {
188-
return pos, err
189-
}
190194
return pos, nil
191195
}
192196
continue

internal/driver/_mysqlcommon/changestream_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,46 @@ func TestParseBinlogRows_NullVsNullString(t *testing.T) {
155155
t.Errorf("quoted 'NULL': name = %#v, want \"NULL\" string", got[1].Values["name"])
156156
}
157157
}
158+
159+
// TestParseBinlogRows_FlushesAtMarker proves a completed event is emitted at the
160+
// next "# at" marker rather than being held until a following ### op-line. In an
161+
// unbounded CDC stream the next op-line may never arrive during a quiet period,
162+
// so without the marker-boundary flush the last change would sit buffered and
163+
// never reach the target. The emit callback fails if it ever sees a pending
164+
// event still buffered when the NEXT marker is processed — i.e. it asserts each
165+
// event is flushed by the marker that immediately follows it, not deferred.
166+
func TestParseBinlogRows_FlushesAtMarker(t *testing.T) {
167+
// Two events separated by markers; the second is followed by a trailing
168+
// marker. If flushing only happened on the next op-line, the second event
169+
// would emit at end-of-loop, after the "# at 300" marker was already seen.
170+
transcript := `# at 4
171+
### INSERT INTO ` + "`test`.`widgets`" + `
172+
### SET
173+
### @1=7
174+
### @2='a'
175+
# at 100
176+
### INSERT INTO ` + "`test`.`widgets`" + `
177+
### SET
178+
### @1=8
179+
### @2='b'
180+
# at 300
181+
`
182+
meta := &tableMetaCache{cache: map[string]*tableLayout{
183+
"widgets": {cols: []string{"id", "name"}, pk: map[string]bool{"id": true}},
184+
}}
185+
186+
var got []canonical.CanonicalChange
187+
emit := func(ch canonical.CanonicalChange) error { got = append(got, ch); return nil }
188+
189+
if _, err := parseBinlogRows(strings.NewReader(transcript), meta, emit, BinlogPosition{File: "mysql-bin.000001"}, nil); err != nil {
190+
t.Fatalf("parseBinlogRows: %v", err)
191+
}
192+
if len(got) != 2 {
193+
t.Fatalf("got %d changes, want 2: %+v", len(got), got)
194+
}
195+
// The first event must be emitted before the second begins — proving the
196+
// "# at 100" marker flushed it rather than the second INSERT's op-line.
197+
if got[0].Values["name"] != "a" || got[1].Values["name"] != "b" {
198+
t.Errorf("changes out of order or wrong: %+v", got)
199+
}
200+
}

0 commit comments

Comments
 (0)