Skip to content

Commit 41f3158

Browse files
committed
redo: release callbacks after size rotation
Release post-flush callbacks as soon as size-rotated redo files are durable. Clear invoked callback slots to avoid retaining receivers through slice capacity, while preserving callback order across concurrent memory-backend uploads.
1 parent e740a10 commit 41f3158

4 files changed

Lines changed: 231 additions & 38 deletions

File tree

pkg/redo/writer/file/file.go

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -228,24 +228,33 @@ func (w *Writer) Run(ctx context.Context) error {
228228
// Write implement write interface
229229
// TODO: more general api with fileName generated by caller
230230
func (w *Writer) Write(rawData []byte) (int, error) {
231+
n, _, err := w.writeRawData(rawData)
232+
return n, err
233+
}
234+
235+
// writeRawData reports whether writing rawData first made the previous file
236+
// durable through a size-triggered rotation.
237+
func (w *Writer) writeRawData(rawData []byte) (int, bool, error) {
231238
w.Lock()
232239
defer w.Unlock()
233240

234241
writeLen := int64(len(rawData))
235242
if writeLen > w.cfg.MaxLogSizeInBytes() {
236-
return 0, errors.ErrRedoFileSizeExceed.GenWithStackByArgs(writeLen, w.cfg.MaxLogSizeInBytes())
243+
return 0, false, errors.ErrRedoFileSizeExceed.GenWithStackByArgs(writeLen, w.cfg.MaxLogSizeInBytes())
237244
}
238245

239246
if w.file == nil {
240247
if err := w.openNew(); err != nil {
241-
return 0, err
248+
return 0, false, err
242249
}
243250
}
244251

252+
rotated := false
245253
if w.size+writeLen > w.cfg.MaxLogSizeInBytes() {
246254
if err := w.rotate(); err != nil {
247-
return 0, err
255+
return 0, false, err
248256
}
257+
rotated = true
249258
}
250259

251260
if w.maxCommitTS.Load() < w.eventCommitTS.Load() {
@@ -254,7 +263,7 @@ func (w *Writer) Write(rawData []byte) (int, error) {
254263
// ref: https://github.com/etcd-io/etcd/pull/5250
255264
lenField, padBytes := writer.EncodeFrameSize(len(rawData))
256265
if err := w.writeUint64(lenField, w.uint64buf); err != nil {
257-
return 0, err
266+
return 0, rotated, err
258267
}
259268

260269
if padBytes != 0 {
@@ -263,12 +272,12 @@ func (w *Writer) Write(rawData []byte) (int, error) {
263272

264273
n, err := w.bw.Write(rawData)
265274
if err != nil {
266-
return 0, err
275+
return 0, rotated, err
267276
}
268277
w.metricWriteBytes.Add(float64(n))
269278
w.size += int64(n)
270279

271-
return n, err
280+
return n, rotated, nil
272281
}
273282

274283
// AdvanceTs implement Advance interface
@@ -327,25 +336,22 @@ func (w *Writer) GetInputCh() chan writer.RedoEvent {
327336
return w.inputCh
328337
}
329338

330-
func (w *Writer) write(event writer.RedoEvent) error {
339+
func (w *Writer) write(event writer.RedoEvent) (bool, error) {
331340
rl := event.ToRedoLog()
332341
if rl.Type == commonEvent.RedoLogTypeDDL {
333342
rl.RedoDDL.SetTableSchemaStore(w.tableSchemaStore)
334343
}
335344
data, err := codec.MarshalRedoLog(rl, nil)
336345
if err != nil {
337-
return errors.WrapError(errors.ErrMarshalFailed, err)
346+
return false, errors.WrapError(errors.ErrMarshalFailed, err)
338347
}
339348
w.AdvanceTs(rl.GetCommitTs())
340-
_, err = w.Write(data)
341-
if err != nil {
342-
return err
343-
}
344-
return nil
349+
_, rotated, err := w.writeRawData(data)
350+
return rotated, err
345351
}
346352

347353
func (w *Writer) SyncWrite(event writer.RedoEvent) error {
348-
err := w.write(event)
354+
_, err := w.write(event)
349355
if err != nil {
350356
return err
351357
}
@@ -364,16 +370,22 @@ func (w *Writer) encode(ctx context.Context) error {
364370
num := 0
365371
flushBatchSize := w.cfg.FlushBatchSize()
366372
cacheEventPostFlush := make([]func(), 0)
373+
runCachedPostFlush := func() {
374+
for i, fn := range cacheEventPostFlush {
375+
// Clear the slot before invocation so retained slice capacity does not
376+
// keep the callback receiver alive after it has been acknowledged.
377+
cacheEventPostFlush[i] = nil
378+
fn()
379+
}
380+
cacheEventPostFlush = cacheEventPostFlush[:0]
381+
}
367382
flush := func() error {
368383
err := w.Flush()
369384
if err != nil {
370385
return err
371386
}
372-
for _, fn := range cacheEventPostFlush {
373-
fn()
374-
}
387+
runCachedPostFlush()
375388
num = 0
376-
cacheEventPostFlush = cacheEventPostFlush[:0]
377389
return nil
378390
}
379391
for {
@@ -386,7 +398,13 @@ func (w *Writer) encode(ctx context.Context) error {
386398
return errors.Trace(err)
387399
}
388400
case e := <-w.inputCh:
389-
err := w.write(e)
401+
rotated, err := w.write(e)
402+
if rotated {
403+
// The previous file is durable before the current event is written,
404+
// so only callbacks accumulated before this event can run here.
405+
runCachedPostFlush()
406+
num = 0
407+
}
390408
if err != nil {
391409
return err
392410
}

pkg/redo/writer/file/file_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/pingcap/ticdc/pkg/fsutil"
2929
"github.com/pingcap/ticdc/pkg/metrics"
3030
"github.com/pingcap/ticdc/pkg/redo"
31+
"github.com/pingcap/ticdc/pkg/redo/codec"
3132
"github.com/pingcap/ticdc/pkg/redo/writer"
3233
"github.com/pingcap/ticdc/pkg/util"
3334
"github.com/pingcap/ticdc/pkg/uuid"
@@ -503,6 +504,71 @@ func TestRunFlushesOnBatchBoundaryAndExecutesPostFlush(t *testing.T) {
503504
require.NoError(t, w.Close())
504505
}
505506

507+
// TestRunReleasesCallbacksAfterSizeRotation writes one event that exactly fills
508+
// a local redo file, confirms its callback remains pending, then writes a second
509+
// event to rotate the first file. The first callback must run after that durable
510+
// rotation while the second event remains unacknowledged in the current file.
511+
func TestRunReleasesCallbacksAfterSizeRotation(t *testing.T) {
512+
dir := t.TempDir()
513+
firstCallbackDone := make(chan struct{})
514+
firstEvent := &pevent.RedoRowEvent{
515+
StartTs: 1,
516+
CommitTs: 1,
517+
Callback: func() {
518+
close(firstCallbackDone)
519+
},
520+
}
521+
encodedFirstEvent, err := codec.MarshalRedoLog(firstEvent.ToRedoLog(), nil)
522+
require.NoError(t, err)
523+
_, padBytes := writer.EncodeFrameSize(len(encodedFirstEvent))
524+
maxLogSizeInBytes := int64(8 + len(encodedFirstEvent) + padBytes)
525+
526+
w, err := newWriter(&localFileConfig{
527+
dir: dir,
528+
maxLogSizeInBytes: maxLogSizeInBytes,
529+
flushIntervalInMs: int64(time.Hour / time.Millisecond),
530+
flushBatchSize: 0,
531+
flushWorkerNum: 1,
532+
}, redo.RedoRowLogFileType, nil)
533+
require.NoError(t, err)
534+
535+
ctx, cancel := context.WithCancel(context.Background())
536+
runErrCh := make(chan error, 1)
537+
go func() {
538+
runErrCh <- w.Run(ctx)
539+
}()
540+
541+
w.GetInputCh() <- firstEvent
542+
require.Eventually(t, func() bool {
543+
return w.eventCommitTS.Load() == firstEvent.CommitTs
544+
}, 10*time.Second, 10*time.Millisecond)
545+
select {
546+
case <-firstCallbackDone:
547+
require.FailNow(t, "callback ran before the file became durable")
548+
default:
549+
}
550+
551+
secondCallbackCount := atomic.NewInt64(0)
552+
secondEvent := &pevent.RedoRowEvent{
553+
StartTs: 2,
554+
CommitTs: 2,
555+
Callback: func() {
556+
secondCallbackCount.Inc()
557+
},
558+
}
559+
w.GetInputCh() <- secondEvent
560+
select {
561+
case <-firstCallbackDone:
562+
case <-time.After(10 * time.Second):
563+
require.FailNow(t, "callback was not released after size rotation")
564+
}
565+
require.Zero(t, secondCallbackCount.Load())
566+
567+
cancel()
568+
require.ErrorIs(t, <-runErrCh, context.Canceled)
569+
require.NoError(t, w.Close())
570+
}
571+
506572
// TestRunDisablesCountBasedFlushWithZero writes more rows than the old fixed
507573
// boundary while using a long ticker interval, then verifies the file backend
508574
// processes them without executing callbacks through a row-count flush.

pkg/redo/writer/memory/file_worker.go

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ type fileCache struct {
4747
filename string
4848
flushed chan struct{}
4949
writer *dataWriter
50+
51+
postFlushCallbacks []func()
5052
}
5153

5254
type dataWriter struct {
@@ -83,6 +85,22 @@ func (f *fileCache) markFlushed() {
8385
}
8486
}
8587

88+
func (f *fileCache) addPostFlushCallback(callback func()) {
89+
if callback != nil {
90+
f.postFlushCallbacks = append(f.postFlushCallbacks, callback)
91+
}
92+
}
93+
94+
// runPostFlushCallbacks clears each slot before invocation so the retained
95+
// slice capacity cannot keep callback receivers alive after the file is durable.
96+
func (f *fileCache) runPostFlushCallbacks() {
97+
for i, callback := range f.postFlushCallbacks {
98+
f.postFlushCallbacks[i] = nil
99+
callback()
100+
}
101+
f.postFlushCallbacks = nil
102+
}
103+
86104
type fileWorkerGroup struct {
87105
cfg *writer.Config
88106
op *writer.LogWriterOptions
@@ -214,23 +232,27 @@ func (f *fileWorkerGroup) bgWriteLogs(
214232
defer ticker.Stop()
215233
num := 0
216234
flushBatchSize := f.cfg.FlushBatchSize()
217-
cacheEventPostFlush := make([]func(), 0)
218235
flush := func() error {
219236
err := f.flushAll(egCtx)
220237
if err != nil {
221238
return err
222239
}
223-
for _, fn := range cacheEventPostFlush {
224-
fn()
225-
}
226240
num = 0
227-
cacheEventPostFlush = cacheEventPostFlush[:0]
228241
return nil
229242
}
230243
for {
244+
// A size-rotated file can finish independently of the current file.
245+
// Release only the durable prefix to preserve input callback order.
246+
f.releaseFlushedFiles()
247+
var firstRotatedFileFlushed <-chan struct{}
248+
if len(f.files) > 1 {
249+
firstRotatedFileFlushed = f.files[0].flushed
250+
}
231251
select {
232252
case <-egCtx.Done():
233253
return errors.Trace(egCtx.Err())
254+
case <-firstRotatedFileFlushed:
255+
continue
234256
case <-ticker.C:
235257
err := flush()
236258
if err != nil {
@@ -241,20 +263,20 @@ func (f *fileWorkerGroup) bgWriteLogs(
241263
log.Error("inputCh of redo file worker is closed unexpectedly")
242264
return errors.ErrUnexpected.FastGenByArgs("inputCh of redo file worker is closed unexpectedly")
243265
}
244-
err := f.writeToCache(egCtx, event)
266+
rotated, err := f.writeToCache(egCtx, event)
245267
if err != nil {
246268
return errors.Trace(err)
247269
}
270+
if rotated {
271+
num = 0
272+
}
248273
num++
249274
// Zero leaves file size and the periodic ticker as the only flush triggers.
250275
if flushBatchSize > 0 && num >= flushBatchSize {
251276
err := flush()
252277
if err != nil {
253278
return errors.Trace(err)
254279
}
255-
event.PostFlush()
256-
} else {
257-
cacheEventPostFlush = append(cacheEventPostFlush, event.PostFlush)
258280
}
259281
}
260282
}
@@ -322,46 +344,48 @@ func (f *fileWorkerGroup) newFileCache(data []byte, commitTs common.Ts) *fileCac
322344

323345
func (f *fileWorkerGroup) writeToCache(
324346
egCtx context.Context, event *polymorphicRedoEvent,
325-
) (err error) {
347+
) (rotated bool, err error) {
326348
commitTs := event.commitTs
327349
data := event.data
328350
if len(data) == 0 {
329-
return errors.ErrUnexpected.FastGenByArgs("encoded redo event data is empty")
351+
return false, errors.ErrUnexpected.FastGenByArgs("encoded redo event data is empty")
330352
}
331353
writeLen := int64(len(data))
332354
if writeLen > f.cfg.MaxLogSizeInBytes() {
333355
// TODO: maybe we need to deal with the oversized commonEvent.
334-
return errors.ErrRedoFileSizeExceed.GenWithStackByArgs(writeLen, f.cfg.MaxLogSizeInBytes())
356+
return false, errors.ErrRedoFileSizeExceed.GenWithStackByArgs(writeLen, f.cfg.MaxLogSizeInBytes())
335357
}
336358
defer f.metricWriteBytes.Add(float64(writeLen))
337359

338360
if len(f.files) == 0 {
339361
file := f.newFileCache(data, commitTs)
340362
if file == nil {
341-
return errors.ErrRedoWriterStopped.FastGenByArgs("failed to create file cache")
363+
return false, errors.ErrRedoWriterStopped.FastGenByArgs("failed to create file cache")
342364
}
365+
file.addPostFlushCallback(event.callback)
343366
f.files = append(f.files, file)
344-
return nil
367+
return false, nil
345368
}
346369

347370
file := f.files[len(f.files)-1]
348371
if file.fileSize+writeLen > f.cfg.MaxLogSizeInBytes() {
349372
select {
350373
case <-egCtx.Done():
351-
return errors.Trace(egCtx.Err())
374+
return false, errors.Trace(egCtx.Err())
352375
case f.flushCh <- file:
353376
}
354377
file := f.newFileCache(data, commitTs)
355378
if file == nil {
356-
return errors.ErrRedoWriterStopped.FastGenByArgs("failed to create file cache")
379+
return false, errors.ErrRedoWriterStopped.FastGenByArgs("failed to create file cache")
357380
}
381+
file.addPostFlushCallback(event.callback)
358382
f.files = append(f.files, file)
359-
return nil
383+
return true, nil
360384
}
361385

362386
_, err = file.writer.Write(data)
363387
if err != nil {
364-
return err
388+
return false, err
365389
}
366390

367391
file.fileSize += writeLen
@@ -371,7 +395,24 @@ func (f *fileWorkerGroup) writeToCache(
371395
if commitTs < file.minCommitTs {
372396
file.minCommitTs = commitTs
373397
}
374-
return nil
398+
file.addPostFlushCallback(event.callback)
399+
return false, nil
400+
}
401+
402+
// releaseFlushedFiles invokes callbacks for the durable prefix of rotated
403+
// files. The last file is still writable and must remain pending until a flush.
404+
func (f *fileWorkerGroup) releaseFlushedFiles() {
405+
for len(f.files) > 1 {
406+
file := f.files[0]
407+
select {
408+
case <-file.flushed:
409+
file.runPostFlushCallbacks()
410+
f.files[0] = nil
411+
f.files = f.files[1:]
412+
default:
413+
return
414+
}
415+
}
375416
}
376417

377418
func (f *fileWorkerGroup) flushAll(egCtx context.Context) error {
@@ -393,6 +434,10 @@ func (f *fileWorkerGroup) flushAll(egCtx context.Context) error {
393434
return errors.Trace(err)
394435
}
395436
}
437+
for _, file := range f.files {
438+
file.runPostFlushCallbacks()
439+
}
440+
clear(f.files)
396441
f.files = f.files[:0]
397442
return nil
398443
}

0 commit comments

Comments
 (0)