Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
14e1fb5
eventstore: support row-level scan resume
asddongmen Jun 26, 2026
a0779ff
eventservice: split large scanned transactions
asddongmen Jun 26, 2026
9605655
eventservice: address large txn spill review comments
asddongmen Jun 27, 2026
f7771ad
eventservice: gate txn split by large txn threshold
asddongmen Jun 29, 2026
e159706
tests: add large txn split integration case
asddongmen Jun 29, 2026
8c0c29c
tests: avoid go dependency in large txn split case
asddongmen Jun 29, 2026
51a44b9
tests: isolate large txn split cdc port
asddongmen Jun 29, 2026
07088cb
tests: reduce large txn split workload logs
asddongmen Jun 29, 2026
f5c2ee9
tests: wait for sync before checking split log
asddongmen Jun 29, 2026
205a7c1
tests: fix large txn split diff config
asddongmen Jun 29, 2026
7652d40
eventcollector,eventservice: add split txn reset coverage
asddongmen Jun 29, 2026
8d6bf1f
eventstore: test row scan position across sub switch
asddongmen Jul 1, 2026
b523967
eventservice: lower large txn threshold default
asddongmen Jul 3, 2026
502c8e2
Merge remote-tracking branch 'upstream/master' into 0626-split-big-txn
asddongmen Jul 6, 2026
c4acc55
tests: cover large txn UK update split
asddongmen Jul 6, 2026
c2b6983
eventservice: fix large txn spill test lint
asddongmen Jul 6, 2026
39be35b
Merge remote-tracking branch 'upstream/master' into 0626-split-big-txn
asddongmen Jul 7, 2026
7d6bdfa
eventservice: avoid redundant scan position copy
asddongmen Jul 7, 2026
284c655
pkg: add reusable spill record files
asddongmen Jul 7, 2026
a2eef5b
eventservice: add big txn scan metrics
asddongmen Jul 7, 2026
dbb2f6e
*: fix go mod tidy
asddongmen Jul 7, 2026
8a8f1eb
Merge remote-tracking branch 'upstream/master' into 0626-split-big-txn
asddongmen Jul 7, 2026
cb1b3a2
spill: fix errors import alias
asddongmen Jul 8, 2026
b758366
eventservice: harden large transaction spill handling
asddongmen Jul 10, 2026
a67388a
eventservice: address large transaction review feedback
asddongmen Jul 10, 2026
c9c0638
eventservice,eventstore: separate scan state and txn strategy
asddongmen Jul 21, 2026
8a9646d
eventservice: encrypt large transaction spill data
asddongmen Jul 21, 2026
a59f875
Merge remote-tracking branch 'upstream/master' into 0626-split-big-txn
asddongmen Jul 21, 2026
ee603b2
eventservice: fix unused parameter lint errors
asddongmen Jul 21, 2026
453a512
eventservice: keep row cursor within scan window
asddongmen Jul 21, 2026
a721f14
eventservice,spill: harden large transaction spill lifecycle
asddongmen Jul 21, 2026
089ba2f
eventservice: fix dispatcher cleanup lifecycle
asddongmen Jul 22, 2026
d1e4204
eventservice: simplify big transaction metric tracking
asddongmen Jul 22, 2026
98c9124
eventservice: clarify row cursor resume range
asddongmen Jul 22, 2026
376646f
eventservice: clarify large transaction scan flow
asddongmen Jul 24, 2026
6be69c5
eventservice: use predefined spill errors
asddongmen Jul 25, 2026
fd4b3a2
eventservice,tests: cover CMEK-encrypted spill files
asddongmen Jul 25, 2026
7e204ba
tests: run CMEK coverage in NextGen heavy CI
asddongmen Jul 25, 2026
4932b7e
tests: provide AWS credentials for CMEK integration
asddongmen Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 428 additions & 0 deletions docs/design/2026-07-22-eventservice-scan-progress-and-txn-strategy.md

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion downstreamadapter/eventcollector/dispatcher_stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package eventcollector
import (
"sync/atomic"

"github.com/pingcap/failpoint"
"github.com/pingcap/log"
"github.com/pingcap/ticdc/downstreamadapter/dispatcher"
"github.com/pingcap/ticdc/pkg/common"
Expand Down Expand Up @@ -333,6 +334,7 @@ func (d *dispatcherStat) isFromCurrentEpoch(event dispatcher.DispatcherEvent, st
// 3. Finally: Forward valid events to target with wake callback
func (d *dispatcherStat) handleBatchDataEvents(events []dispatcher.DispatcherEvent) bool {
var validEvents []dispatcher.DispatcherEvent
hasDML := false
state := d.loadCurrentEpochState()
for _, event := range events {
if !d.isFromCurrentEpoch(event, state) {
Expand All @@ -354,6 +356,7 @@ func (d *dispatcherStat) handleBatchDataEvents(events []dispatcher.DispatcherEve
validEvents = append(validEvents, event)
case commonEvent.TypeDMLEvent:
if d.shouldForwardEventByCommitTs(event) {
hasDML = true
validEvents = append(validEvents, event)
}
case commonEvent.TypeBatchDMLEvent:
Expand All @@ -374,6 +377,7 @@ func (d *dispatcherStat) handleBatchDataEvents(events []dispatcher.DispatcherEve
dml.TableInfoVersion = tableInfoVersion
dmlEvent := dispatcher.NewDispatcherEvent(event.From, dml)
if d.shouldForwardEventByCommitTs(dmlEvent) {
hasDML = true
validEvents = append(validEvents, dmlEvent)
}
}
Expand All @@ -388,7 +392,17 @@ func (d *dispatcherStat) handleBatchDataEvents(events []dispatcher.DispatcherEve
return false
}
d.updateCommitTsStateByEvents(state, validEvents)
return d.target.HandleEvents(validEvents, func() { d.wake() })
handled := d.target.HandleEvents(validEvents, func() { d.wake() })
if hasDML {
failpoint.Inject("InjectResetDispatcherAfterBatchDataEvents", func() {
log.Info("inject dispatcher reset after batch data events",
zap.Stringer("changefeedID", d.target.GetChangefeedID()),
zap.Stringer("dispatcherID", d.getDispatcherID()),
zap.Uint64("checkpointTs", d.target.GetCheckpointTs()))
d.session.resetCurrentEventService()
})
}
return handled
}

// handleSingleDataEvents processes a single DDL or SyncPoint event with the following algorithm:
Expand Down
39 changes: 39 additions & 0 deletions downstreamadapter/eventcollector/dispatcher_stat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"testing"
"time"

"github.com/pingcap/failpoint"
"github.com/pingcap/ticdc/downstreamadapter/dispatcher"
"github.com/pingcap/ticdc/eventpb"
"github.com/pingcap/ticdc/heartbeatpb"
Expand Down Expand Up @@ -1277,6 +1278,44 @@ func TestHandleBatchDataEvents(t *testing.T) {
}
}

func TestInjectResetDispatcherAfterBatchDataEvents(t *testing.T) {
failpointName := "github.com/pingcap/ticdc/downstreamadapter/eventcollector/InjectResetDispatcherAfterBatchDataEvents"
require.NoError(t, failpoint.Enable(failpointName, `1*return(true)`))
defer func() {
require.NoError(t, failpoint.Disable(failpointName))
}()

localServerID := node.ID("local-server")
dispatcherID := common.NewDispatcherID()
mockDisp := newMockDispatcher(dispatcherID, 100)
mockDisp.handleEvents = func(events []dispatcher.DispatcherEvent, wakeCallback func()) (block bool) {
return len(events) > 0
}
collector := newTestEventCollector(localServerID)
stat := newDispatcherStat(mockDisp, collector, nil)
stat.currentEpoch.Store(newDispatcherEpochState(1, 1, stat.target.GetStartTs()))
stat.lastEventCommitTs.Store(100)
markSessionReceiving(stat.session, localServerID)

require.True(t, stat.handleBatchDataEvents([]dispatcher.DispatcherEvent{
{
From: &localServerID,
Event: &commonEvent.DMLEvent{
Seq: 2,
Epoch: 1,
CommitTs: 101,
},
},
}))
requireDispatcherRequests(
t,
readDispatcherRequests(t, collector, 1),
dispatcherRequestRecord{to: localServerID, action: eventpb.ActionType_ACTION_TYPE_RESET},
)
require.Equal(t, uint64(2), stat.loadCurrentEpochState().epoch)
require.Equal(t, uint64(101), stat.loadCurrentEpochState().maxEventTs.Load())
}

func TestHandleSingleDataEvents(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require (
github.com/pingcap/tidb/pkg/parser v0.0.0-20260604031706-f9faeaf4828f
github.com/pingcap/tiflow v0.0.0-20260610095716-97d622547231
github.com/prometheus/client_golang v1.23.0
github.com/prometheus/client_model v0.6.2
github.com/r3labs/diff v1.1.0
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
github.com/robfig/cron v1.2.0
Expand Down Expand Up @@ -298,7 +299,6 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
Expand Down
58 changes: 42 additions & 16 deletions logservice/eventstore/event_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ type EventStore interface {

UpdateDispatcherCheckpointTs(dispatcherID common.DispatcherID, checkpointTs uint64)

// GetIterator returns an iterator which scans data in ts range (dataRange.CommitTsStart, dataRange.CommitTsEnd].
GetIterator(dispatcherID common.DispatcherID, dataRange common.DataRange) (EventIterator, error)
// GetIterator returns an iterator for the requested range and resume cursor.
GetIterator(dispatcherID common.DispatcherID, request ScanRequest) (EventIterator, error)

GetLogCoordinatorNodeID() node.ID
}
Expand All @@ -115,6 +115,14 @@ type EventIterator interface {
Close() (eventCnt int64, err error)
}

type EventIteratorWithScanPosition interface {
EventIterator

// NextWithScanPosition returns the next event, the opaque position of the
// returned event, and whether this event is from a new txn.
NextWithScanPosition() (*common.RawKVEntry, ScanPosition, bool)
}

type dispatcherStat struct {
dispatcherID common.DispatcherID
// data span of this dispatcher
Expand Down Expand Up @@ -809,10 +817,11 @@ func (e *eventStore) UpdateDispatcherCheckpointTs(
updateSubStatCheckpoint(dispatcherStat.removingSubStat)
}

func (e *eventStore) GetIterator(dispatcherID common.DispatcherID, dataRange common.DataRange) (EventIterator, error) {
func (e *eventStore) GetIterator(dispatcherID common.DispatcherID, request ScanRequest) (EventIterator, error) {
if e.closed.Load() {
return nil, nil
}
dataRange := request.Range

e.dispatcherMeta.RLock()
stat, ok := e.dispatcherMeta.dispatcherStats[dispatcherID]
Expand All @@ -830,7 +839,7 @@ func (e *eventStore) GetIterator(dispatcherID common.DispatcherID, dataRange com
zap.Int64("tableID", dataRange.Span.GetTableID()),
zap.Uint64("commitTsStart", dataRange.CommitTsStart),
zap.Uint64("commitTsEnd", dataRange.CommitTsEnd),
zap.Uint64("lastScannedTxnStartTs", dataRange.LastScannedTxnStartTs))
zap.Uint64("lastScannedTxnStartTs", request.Cursor.TxnStartTs))
}
return nil
}
Expand All @@ -841,7 +850,7 @@ func (e *eventStore) GetIterator(dispatcherID common.DispatcherID, dataRange com
zap.Int64("tableID", dataRange.Span.GetTableID()),
zap.Uint64("commitTsStart", dataRange.CommitTsStart),
zap.Uint64("commitTsEnd", dataRange.CommitTsEnd),
zap.Uint64("lastScannedTxnStartTs", dataRange.LastScannedTxnStartTs),
zap.Uint64("lastScannedTxnStartTs", request.Cursor.TxnStartTs),
zap.Uint64("subStatCheckpointTs", checkpointTs),
zap.Uint64("subStatResolvedTs", subStat.resolvedTs.Load()))
}
Expand All @@ -852,7 +861,7 @@ func (e *eventStore) GetIterator(dispatcherID common.DispatcherID, dataRange com
zap.Int64("tableID", dataRange.Span.GetTableID()),
zap.Uint64("commitTsStart", dataRange.CommitTsStart),
zap.Uint64("commitTsEnd", dataRange.CommitTsEnd),
zap.Uint64("lastScannedTxnStartTs", dataRange.LastScannedTxnStartTs),
zap.Uint64("lastScannedTxnStartTs", request.Cursor.TxnStartTs),
zap.Uint64("subStatCheckpointTs", checkpointTs),
zap.Uint64("subStatResolvedTs", subStat.resolvedTs.Load()))
}
Expand Down Expand Up @@ -909,29 +918,38 @@ func (e *eventStore) GetIterator(dispatcherID common.DispatcherID, dataRange com
e.dispatcherMeta.Unlock()
}

// dataRange fields:
// CommitTsStart and CommitTsEnd define the commit-ts scan window.
// LastScannedTxnStartTs records how far the previous scan progressed inside
// request fields:
// Range defines the commit-ts scan window. Cursor.TxnStartTs records how far
// the previous scan progressed inside
// CommitTsStart. It is zero if there is no unfinished scan at CommitTsStart.
//
// Iterator key bounds:
// Pebble uses [LowerBound, UpperBound), so end is always encoded as
// CommitTsEnd+1.
//
// If LastScannedTxnStartTs is zero, scan commit ts in
// If Cursor.Position is present, continue scanning from the next
// eventstore key after that opaque position.
//
// If Cursor.TxnStartTs is zero, scan commit ts in
// (CommitTsStart, CommitTsEnd], and use CommitTsStart+1 as LowerBound.
//
// If LastScannedTxnStartTs is non-zero, continue scanning commit ts
// CommitTsStart with start ts greater than LastScannedTxnStartTs, then scan
// If Cursor.TxnStartTs is non-zero, continue scanning commit ts
// CommitTsStart with start ts greater than Cursor.TxnStartTs, then scan
// later commit ts up to CommitTsEnd.
//
var start []byte
if dataRange.LastScannedTxnStartTs != 0 {
if len(request.Cursor.Position) != 0 {
start = encodeRowLevelScanPositionLowerBound(
uint64(subStat.subID),
stat.tableSpan.TableID,
request.Cursor.Position,
)
} else if request.Cursor.TxnStartTs != 0 {
start = encodeScanLowerBound(
uint64(subStat.subID),
stat.tableSpan.TableID,
dataRange.CommitTsStart,
dataRange.LastScannedTxnStartTs+1,
request.Cursor.TxnStartTs+1,
)
} else {
start = encodeTxnCommitTsBoundaryKey(uint64(subStat.subID), stat.tableSpan.TableID, dataRange.CommitTsStart+1)
Expand Down Expand Up @@ -1563,10 +1581,16 @@ type eventStoreIter struct {
}

func (iter *eventStoreIter) Next() (*common.RawKVEntry, bool) {
rawKV, _, isNewTxn := iter.NextWithScanPosition()
return rawKV, isNewTxn
}

func (iter *eventStoreIter) NextWithScanPosition() (*common.RawKVEntry, ScanPosition, bool) {
rawKV := &common.RawKVEntry{}
var scanPosition ScanPosition
for {
if !iter.innerIter.Valid() {
return nil, false
return nil, nil, false
}
key := iter.innerIter.Key()
value := iter.innerIter.Value()
Expand Down Expand Up @@ -1605,11 +1629,13 @@ func (iter *eventStoreIter) Next() (*common.RawKVEntry, bool) {
}
scannedBytesMetrics.Add(float64(len(value)))
if !iter.needCheckSpan {
scanPosition = encodeRowLevelScanPosition(key)
break
}
comparableKey := common.ToComparableKey(rawKV.Key)
if bytes.Compare(comparableKey, iter.tableSpan.StartKey) >= 0 &&
bytes.Compare(comparableKey, iter.tableSpan.EndKey) < 0 {
scanPosition = encodeRowLevelScanPosition(key)
break
}
log.Debug("event store iter skip kv not in table span",
Expand All @@ -1635,7 +1661,7 @@ func (iter *eventStoreIter) Next() (*common.RawKVEntry, bool) {
startTime := time.Now()
iter.innerIter.Next()
metricEventStoreNextReadDurationHistogram.Observe(time.Since(startTime).Seconds())
return rawKV, isNewTxn
return rawKV, scanPosition, isNewTxn
}

func (iter *eventStoreIter) Close() (int64, error) {
Expand Down
Loading
Loading