Skip to content

eventservice: split large scanned transactions#5511

Open
asddongmen wants to merge 23 commits into
pingcap:masterfrom
asddongmen:0626-split-big-txn
Open

eventservice: split large scanned transactions#5511
asddongmen wants to merge 23 commits into
pingcap:masterfrom
asddongmen:0626-split-big-txn

Conversation

@asddongmen

@asddongmen asddongmen commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

What problem does this PR solve?

Issue Number: close #5596

EventBroker currently scans a whole large upstream transaction into memory before it can send DML to the event collector. Very large transactions can therefore cause OOM, especially when UK-changing updates cache deferred insert rows in memory.

What is changed and how it works?

This PR adds row-level eventstore scan resume tokens and uses them in EventBroker scanning so transaction-atomicity=none can emit bounded fragments from one large transaction without sending resolved-ts for that commit-ts early.

It also spills deferred insert halves of UK-changing updates to local disk, drains them after the original delete phase, and cleans spill state on reset/remove. EventCollector reset semantics remain checkpoint-ts based.

Check List

Tests

  • Unit test

Questions

Will it cause performance regression or break compatibility?

No compatibility break is expected. The split path is only enabled when transaction atomicity allows splitting. It reduces EventBroker peak memory for large transactions, with local spill I/O for UK-changing update insert halves.

Do you need to update user documentation, design documentation or monitoring documentation?

No user documentation update is required for this internal scan behavior change.

Release note

Support splitting large transactions during EventBroker DML scanning to reduce OOM risk.

Summary by CodeRabbit

  • New Features
    • Added row-level scan resume support using opaque scan positions.
    • Introduced large-transaction spill-to-disk behavior and progress persistence.
    • Added “big transaction” Prometheus metrics.
  • Bug Fixes
    • Improved event scanning/broker progress handling to continue correctly after resumption and internal lifecycle transitions.
    • Added controlled dispatcher reset after batch event handling (with failpoint).
  • Configuration
    • Added large-txn-threshold-in-bytes with a 1MB default.
  • Tests
    • Expanded coverage for row-level resume, large-txn splitting/spill cleanup, dispatcher lifecycle cleanup, and dispatcher reset behavior.

@ti-chi-bot

ti-chi-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-linked-issue release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. labels Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08f16171-66a3-4aca-924e-c5c5e266a1e8

📥 Commits

Reviewing files that changed from the base of the PR and between 8a8f1eb and cb1b3a2.

📒 Files selected for processing (1)
  • pkg/spill/record_file.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/spill/record_file.go

📝 Walkthrough

Walkthrough

This PR adds row-level scan resumption and large-transaction spill handling across eventstore, eventservice, and event broker paths. It also adds dispatcher big-txn metrics, an integration test for large transaction splitting, a failpoint-triggered dispatcher reset, and a go.mod dependency reorder.

Changes

Large Transaction Split & Spill

Layer / File(s) Summary
Scan position and config contracts
pkg/common/table_span.go, pkg/config/debug.go, pkg/config/server_config_test.go, pkg/errors/error.go
Adds ScanPosition, DataRange.RowLevelScanPosition, LargeTxnThresholdInBytes, and ErrSpillFileOp.
EventStore row-level scan resumption
logservice/eventstore/event_store.go, logservice/eventstore/format.go, logservice/eventstore/pebble_test.go, logservice/eventstore/event_store_test.go
Adds scan-position-aware iterator APIs and resume bounds, with tests for row-level continuation.
Generic spill record file
pkg/spill/record_file.go, pkg/spill/record_file_test.go
Adds length-prefixed record spill storage with random access, sequential reading, and cleanup.
Large-txn insert spill and phase state
pkg/eventservice/large_txn_spill.go, pkg/eventservice/large_txn_spill_test.go, pkg/eventservice/large_txn_state.go
Adds spill-backed large-txn insert storage and dispatcher-managed phase/cleanup state.
Dispatcher scan range and big-txn metrics
pkg/eventservice/dispatcher_stat.go, pkg/eventservice/dispatcher_stat_test.go, pkg/eventservice/metrics_collector.go, pkg/metrics/event_service.go
Adds row-level scan progress storage, big-txn metric aggregation, and Prometheus big-txn metrics.
Event scanner: row-level progress and spill-driven DML processing
pkg/eventservice/event_scanner.go, pkg/eventservice/event_scanner_test.go, pkg/eventservice/event_scanner_benchmark_test.go
Returns scan progress from scanning, tracks row-level positions, spills large split-update inserts, and drains spilled inserts.
Event broker wiring and lifecycle cleanup
pkg/eventservice/event_broker.go, pkg/eventservice/event_broker_test.go, pkg/eventservice/event_service_test.go
Persists scan progress, resumes empty windows under row-level/large-txn conditions, and cleans up large-txn state on dispatcher lifecycle changes.
Large-txn-split integration test
tests/integration_tests/large_txn_split/*, tests/integration_tests/run_light_it_in_ci.sh
Adds the large-txn-split integration workflow and registers it in CI.

Dispatcher batch-reset failpoint

Layer / File(s) Summary
Failpoint-triggered dispatcher reset
downstreamadapter/eventcollector/dispatcher_stat.go, downstreamadapter/eventcollector/dispatcher_stat_test.go
Tracks forwarded DML in a batch and injects a reset of the current event service through a failpoint, with test coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EventBroker
  participant EventScanner
  participant DispatcherStat
  participant EventStore
  participant LargeTxnSpill

  EventBroker->>DispatcherStat: getDataRange()/getScanTaskDataRange()
  DispatcherStat-->>EventBroker: DataRange (RowLevelScanPosition)
  EventBroker->>EventScanner: scan(dataRange)
  EventScanner->>EventStore: GetIterator(dataRange)
  EventStore-->>EventScanner: NextWithScanPosition()
  EventScanner->>LargeTxnSpill: appendInsert(row)
  EventScanner-->>EventBroker: events, scanProgress
  EventBroker->>DispatcherStat: updateScanRangeWithPosition(progress)
  EventBroker->>EventBroker: persist resume state
Loading

Possibly related PRs

  • pingcap/ticdc#4579: Also changes downstreamadapter/eventcollector/dispatcher_stat.go batch forwarding behavior.
  • pingcap/ticdc#4953: Also changes logservice/eventstore iterator bounds and encoding.
  • pingcap/ticdc#5547: Also changes pkg/eventservice/event_broker.go scan-range advancement logic.

Suggested labels: lgtm, approved

Suggested reviewers: lidezhu, hongyunyan, wk989898

Poem

A rabbit hopped through txns so wide,
with scan tokens tucked inside.
It spilled the heavy roots away,
then resumed at break of day.
🐇📦

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: splitting large scanned transactions in eventservice.
Description check ✅ Passed The description includes the issue number, problem, changes, tests, compatibility notes, documentation note, and release note.
Linked Issues check ✅ Passed The code implements row-level resume tokens and spill-to-disk handling to reduce large-transaction scan memory, matching issue #5596.
Out of Scope Changes check ✅ Passed The changes shown are all aligned with large-transaction splitting, resumption, spill cleanup, testing, and required supporting config updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ti-chi-bot ti-chi-bot Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Jun 26, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for row-level resume tokens and large transaction spilling in the event service. It adds a new EventIteratorWithScanPosition interface, updates the event store iterator to track and return opaque scan positions, and implements disk-based spilling for large transactions via largeTxnInsertSpill and largeTxnScanState. Review feedback identifies two key issues in large_txn_spill.go: a potential resource leak where a failed Close() in Cleanup() skips deleting the temporary file, and a loose record length check that could lead to OOM panics if the spill file is corrupted.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/eventservice/large_txn_spill.go Outdated
Comment thread pkg/eventservice/large_txn_spill.go Outdated
@ti-chi-bot

ti-chi-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign lidezhu for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Signed-off-by: dongmen <414110582@qq.com>
# Conflicts:
#	pkg/eventservice/event_broker_test.go
@asddongmen asddongmen marked this pull request as ready for review July 7, 2026 07:37
@ti-chi-bot ti-chi-bot Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 7, 2026
@asddongmen

Copy link
Copy Markdown
Collaborator Author

/test all

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/eventservice/event_scanner.go (1)

336-340: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Set session.progress before returning here pkg/eventservice/event_scanner.go:336-340interruptScan() only appends events, but scan() returns sess.progress; this branch can emit a zero-value checkpoint and break resume handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/event_scanner.go` around lines 336 - 340, The interrupt path
in scan() currently returns before session.progress is updated, so the
checkpoint can remain stale or zero-valued. In the branch that calls
interruptScan() in event_scanner.go, assign session.progress from the current
rawEvent state before returning, using the same progress fields that scan()
later emits so resumed scans continue from the correct checkpoint.
🧹 Nitpick comments (6)
tests/integration_tests/large_txn_split/run.sh (2)

13-14: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Unquoted rm -rf $WORK_DIR flagged by static analysis.

Static analysis flags the unquoted expansion in rm -rf $WORK_DIR as a potential path-injection/word-splitting risk. WORK_DIR is test-harness controlled here, but quoting is a trivial, no-risk hardening matching the also-flagged SC2086 hints on lines 14-25, 80, 98, 114-117.

🔧 Proposed quoting fix
-	rm -rf $WORK_DIR && mkdir -p $WORK_DIR
+	rm -rf -- "$WORK_DIR" && mkdir -p "$WORK_DIR"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration_tests/large_txn_split/run.sh` around lines 13 - 14, The
shell script has an unquoted variable expansion in the `prepare` function that
triggers static analysis warnings; update the `rm -rf $WORK_DIR` usage to quote
`WORK_DIR` consistently with the other SC2086-related command invocations in
this script. Apply the same quoting pattern anywhere `WORK_DIR` is expanded in
`prepare` and the related test helpers so the shell treats it as a single path
argument.

Source: Linters/SAST tools


98-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

prepare $* passes unused arguments.

prepare() (lines 13-32) takes no parameters and never references $1/$@, so prepare $* is misleading; also flagged by shellcheck (SC2048) for missing quoting.

🔧 Proposed cleanup
-	prepare $*
+	prepare
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration_tests/large_txn_split/run.sh` at line 98, The `prepare $*`
call in `run.sh` is passing unused positional arguments to `prepare()`, which
takes no parameters and does not reference them. Update the call site near
`prepare` to invoke it without forwarding `$*`, and keep the existing `prepare`
function signature as-is unless you actually intend to consume arguments.

Source: Linters/SAST tools

pkg/eventservice/dispatcher_stat.go (1)

234-252: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Copy scan-position slices at the state boundary.

common.ScanPosition is a mutable []byte; storing and returning the same backing array lets later caller mutations corrupt the dispatcher’s resume cursor.

Suggested fix
 func (a *dispatcherStat) storeLastScannedPosition(position common.ScanPosition) {
 	if len(position) == 0 {
 		a.lastScannedPosition.Store(common.ScanPosition{})
 		return
 	}
-	// ScanPosition is produced as an immutable snapshot by the scanner path.
-	a.lastScannedPosition.Store(position)
+	a.lastScannedPosition.Store(append(common.ScanPosition(nil), position...))
 }
 
 func (a *dispatcherStat) getLastScannedPosition() common.ScanPosition {
 	value := a.lastScannedPosition.Load()
 	if value == nil {
@@
 	if len(position) == 0 {
 		return nil
 	}
-	return position
+	return append(common.ScanPosition(nil), position...)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/dispatcher_stat.go` around lines 234 - 252, The
dispatcherStat scan-position handling is leaking a mutable common.ScanPosition
slice across the state boundary, so caller mutations can corrupt the stored
resume cursor. Update storeLastScannedPosition and getLastScannedPosition to
make defensive copies when storing into lastScannedPosition and when returning
from it, rather than keeping or exposing the same backing array. Use the
dispatcherStat methods and the common.ScanPosition type to locate the fix.
logservice/eventstore/format.go (1)

102-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Successor-byte technique is correct.

Manually rebuilding prefix + position + 0x00 to produce a strict successor key matches the same append-0x00 pattern Pebble's own default comparer uses for ImmediateSuccessor. Confirmed via Pebble source. Logic checks out against the pebble_test.go assertions.

Minor optional note: this duplicates the uniqueID/tableID prefix-writing already done in encodeTxnCommitTsBoundaryKeyTo (line 83-87 area). Could factor out a small writeKeyPrefix(buf, uniqueID, tableID) helper shared by both call sites to avoid drift if the prefix layout changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/eventstore/format.go` around lines 102 - 108, The successor-byte
logic in encodeRowLevelScanPositionLowerBound is fine, but the uniqueID/tableID
prefix writing is duplicated from encodeTxnCommitTsBoundaryKeyTo. Extract the
shared prefix construction into a small helper such as a writeKeyPrefix function
and use it in both places so the key layout stays consistent if the prefix
format changes.
pkg/eventservice/event_service_test.go (1)

384-389: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add a length guard to avoid a panic on malformed positions.

binary.BigEndian.Uint64 will panic if position is non-empty but shorter than 8 bytes. Currently only internally-produced 8-byte positions reach this function, but a defensive check would avoid future breakage if the mock is reused with externally-supplied positions.

🛡️ Suggested guard
 func decodeMockScanPosition(position common.ScanPosition) int {
-	if len(position) == 0 {
+	if len(position) != 8 {
 		return -1
 	}
 	return int(binary.BigEndian.Uint64(position))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/event_service_test.go` around lines 384 - 389, Add a
defensive length check in decodeMockScanPosition so it only calls
binary.BigEndian.Uint64 when common.ScanPosition has at least 8 bytes; otherwise
return the sentinel value already used for empty input. Keep the existing
behavior for valid 8-byte positions, and make the guard explicit in this helper
so future callers of decodeMockScanPosition do not panic on malformed or
externally supplied positions.
pkg/eventservice/event_broker.go (1)

1127-1132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate cleanup+log pattern across three call sites.

The cleanup-and-warn block is repeated verbatim (differing only in the log message) in removeDispatcher, resetDispatcher's initial path, and its CAS-retry loop. Consider extracting a small helper, e.g. logLargeTxnCleanupErr(stat, dispatcherInfo, context).

♻️ Suggested helper
func (c *eventBroker) cleanupLargeTxnStateLogged(stat *dispatcherStat, dispatcherInfo DispatcherInfo, action string) {
	if err := stat.cleanupLargeTxnState(); err != nil {
		log.Warn("cleanup large txn state failed",
			zap.String("action", action),
			zap.Stringer("changefeedID", dispatcherInfo.GetChangefeedID()),
			zap.Stringer("dispatcherID", dispatcherInfo.GetID()),
			zap.Error(err))
	}
}

Also applies to: 1206-1211, 1262-1267

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/event_broker.go` around lines 1127 - 1132, The
cleanup-and-warn block around stat.cleanupLargeTxnState() is duplicated in
removeDispatcher and both resetDispatcher paths, so extract it into a small
helper on eventBroker (for example cleanupLargeTxnStateLogged or
logLargeTxnCleanupErr) and call that from each site. Have the helper accept the
dispatcher state plus dispatcherInfo and an action/context string, then
centralize the log.Warn call there while preserving the existing changefeedID,
dispatcherID, and error fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/common/table_span.go`:
- Around line 62-65: Update the DataRange equality logic in the table span
comparison so it also includes LastScannedTxnStartTs alongside Span,
CommitTsStart, CommitTsEnd, and RowLevelScanPosition. The issue is in the
equality method that currently returns true for ranges with different
transaction resume cursors when RowLevelScanPosition is empty; add
LastScannedTxnStartTs to the same comparison in the DataRange/TableSpan equality
path so distinct iterator lower bounds no longer compare equal.

In `@pkg/eventservice/dispatcher_stat.go`:
- Around line 271-279: The big transaction metric state is being replaced in the
dispatcher without first emitting the currently pending metric, which drops data
when a new (startTs, commitTs) pair arrives. Update the logic in the
bigTxnMetricState handling block to flush/emit the existing state before
assigning a new bigTxnMetricState, using the same code path that records the
metric so the pending value is not lost.

In `@pkg/eventservice/large_txn_spill.go`:
- Around line 30-48: newLargeTxnInsertSpill and largeTxnInsertSpill.Append are
creating ad-hoc errors with errors.New instead of using the repository’s
predefined spill-file error. Update these paths to return the existing
spill-related error type (for example cerror.ErrSpillFileOp) and preserve the
specific context for empty dir, record file creation failure, and nil entry
cases. Use the symbols newLargeTxnInsertSpill, Append, and cerror.ErrSpillFileOp
to locate and replace the generic error creation consistently.

In `@pkg/eventservice/large_txn_state.go`:
- Around line 61-65: The large-txn state checks in large_txn_state.go are
returning ad-hoc errors instead of the repository’s predefined error types.
Update the mismatch/cleanup/wrong-phase paths in the relevant large txn state
helpers (including the state comparison block and the related cases in the same
file) to return the existing shared errors used by large_txn_spill.go, and
follow the error-handling guidance in docs/agents/error-handling.md so callers
can reliably match these conditions.

In `@pkg/spill/record_file.go`:
- Around line 129-137: The deferred close on the freshly opened spill file
handle in the spill file open path ignores a possible error, which is what the
CI check is flagging. Update the `recordFile` open logic so the `os.Open` handle
is closed with error handling rather than a bare `defer file.Close()`, and make
sure any close failure is handled or logged consistently with the existing error
wrapping used in `cerror.WrapError` and the `spill` file access flow.
- Around line 16-22: The import in record_file.go aliases
github.com/pingcap/ticdc/pkg/errors as cerror even though there is no local
package name conflict, so it should be imported unaliased to satisfy the
importas rule. Update the import block in the RecordFile code accordingly, then
rename every cerror usage in this file to the package name errors, including the
error constants and helpers used in the RecordFile methods.

---

Outside diff comments:
In `@pkg/eventservice/event_scanner.go`:
- Around line 336-340: The interrupt path in scan() currently returns before
session.progress is updated, so the checkpoint can remain stale or zero-valued.
In the branch that calls interruptScan() in event_scanner.go, assign
session.progress from the current rawEvent state before returning, using the
same progress fields that scan() later emits so resumed scans continue from the
correct checkpoint.

---

Nitpick comments:
In `@logservice/eventstore/format.go`:
- Around line 102-108: The successor-byte logic in
encodeRowLevelScanPositionLowerBound is fine, but the uniqueID/tableID prefix
writing is duplicated from encodeTxnCommitTsBoundaryKeyTo. Extract the shared
prefix construction into a small helper such as a writeKeyPrefix function and
use it in both places so the key layout stays consistent if the prefix format
changes.

In `@pkg/eventservice/dispatcher_stat.go`:
- Around line 234-252: The dispatcherStat scan-position handling is leaking a
mutable common.ScanPosition slice across the state boundary, so caller mutations
can corrupt the stored resume cursor. Update storeLastScannedPosition and
getLastScannedPosition to make defensive copies when storing into
lastScannedPosition and when returning from it, rather than keeping or exposing
the same backing array. Use the dispatcherStat methods and the
common.ScanPosition type to locate the fix.

In `@pkg/eventservice/event_broker.go`:
- Around line 1127-1132: The cleanup-and-warn block around
stat.cleanupLargeTxnState() is duplicated in removeDispatcher and both
resetDispatcher paths, so extract it into a small helper on eventBroker (for
example cleanupLargeTxnStateLogged or logLargeTxnCleanupErr) and call that from
each site. Have the helper accept the dispatcher state plus dispatcherInfo and
an action/context string, then centralize the log.Warn call there while
preserving the existing changefeedID, dispatcherID, and error fields.

In `@pkg/eventservice/event_service_test.go`:
- Around line 384-389: Add a defensive length check in decodeMockScanPosition so
it only calls binary.BigEndian.Uint64 when common.ScanPosition has at least 8
bytes; otherwise return the sentinel value already used for empty input. Keep
the existing behavior for valid 8-byte positions, and make the guard explicit in
this helper so future callers of decodeMockScanPosition do not panic on
malformed or externally supplied positions.

In `@tests/integration_tests/large_txn_split/run.sh`:
- Around line 13-14: The shell script has an unquoted variable expansion in the
`prepare` function that triggers static analysis warnings; update the `rm -rf
$WORK_DIR` usage to quote `WORK_DIR` consistently with the other SC2086-related
command invocations in this script. Apply the same quoting pattern anywhere
`WORK_DIR` is expanded in `prepare` and the related test helpers so the shell
treats it as a single path argument.
- Line 98: The `prepare $*` call in `run.sh` is passing unused positional
arguments to `prepare()`, which takes no parameters and does not reference them.
Update the call site near `prepare` to invoke it without forwarding `$*`, and
keep the existing `prepare` function signature as-is unless you actually intend
to consume arguments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d2dd6fe-80ac-444b-83dd-e765ee0225f8

📥 Commits

Reviewing files that changed from the base of the PR and between 040cb1e and 8a8f1eb.

📒 Files selected for processing (30)
  • downstreamadapter/eventcollector/dispatcher_stat.go
  • downstreamadapter/eventcollector/dispatcher_stat_test.go
  • go.mod
  • logservice/eventstore/event_store.go
  • logservice/eventstore/event_store_test.go
  • logservice/eventstore/format.go
  • logservice/eventstore/pebble_test.go
  • pkg/common/table_span.go
  • pkg/config/debug.go
  • pkg/config/server_config_test.go
  • pkg/errors/error.go
  • pkg/eventservice/dispatcher_stat.go
  • pkg/eventservice/dispatcher_stat_test.go
  • pkg/eventservice/event_broker.go
  • pkg/eventservice/event_broker_test.go
  • pkg/eventservice/event_scanner.go
  • pkg/eventservice/event_scanner_benchmark_test.go
  • pkg/eventservice/event_scanner_test.go
  • pkg/eventservice/event_service_test.go
  • pkg/eventservice/large_txn_spill.go
  • pkg/eventservice/large_txn_spill_test.go
  • pkg/eventservice/large_txn_state.go
  • pkg/eventservice/metrics_collector.go
  • pkg/metrics/event_service.go
  • pkg/spill/record_file.go
  • pkg/spill/record_file_test.go
  • tests/integration_tests/large_txn_split/conf/diff_config.toml
  • tests/integration_tests/large_txn_split/conf/server.toml
  • tests/integration_tests/large_txn_split/run.sh
  • tests/integration_tests/run_light_it_in_ci.sh

Comment thread pkg/common/table_span.go
Comment on lines +62 to +65
return d.Span.Equal(other.Span) &&
d.CommitTsStart == other.CommitTsStart &&
d.CommitTsEnd == other.CommitTsEnd &&
bytes.Equal(d.RowLevelScanPosition, other.RowLevelScanPosition)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include LastScannedTxnStartTs in range equality.

LastScannedTxnStartTs changes the iterator lower bound when RowLevelScanPosition is empty, so two DataRanges with different transaction-level resume cursors can currently compare equal.

Suggested fix
 	return d.Span.Equal(other.Span) &&
 		d.CommitTsStart == other.CommitTsStart &&
 		d.CommitTsEnd == other.CommitTsEnd &&
-		bytes.Equal(d.RowLevelScanPosition, other.RowLevelScanPosition)
+		d.LastScannedTxnStartTs == other.LastScannedTxnStartTs &&
+		bytes.Equal(d.RowLevelScanPosition, other.RowLevelScanPosition)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return d.Span.Equal(other.Span) &&
d.CommitTsStart == other.CommitTsStart &&
d.CommitTsEnd == other.CommitTsEnd &&
bytes.Equal(d.RowLevelScanPosition, other.RowLevelScanPosition)
return d.Span.Equal(other.Span) &&
d.CommitTsStart == other.CommitTsStart &&
d.CommitTsEnd == other.CommitTsEnd &&
d.LastScannedTxnStartTs == other.LastScannedTxnStartTs &&
bytes.Equal(d.RowLevelScanPosition, other.RowLevelScanPosition)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/common/table_span.go` around lines 62 - 65, Update the DataRange equality
logic in the table span comparison so it also includes LastScannedTxnStartTs
alongside Span, CommitTsStart, CommitTsEnd, and RowLevelScanPosition. The issue
is in the equality method that currently returns true for ranges with different
transaction resume cursors when RowLevelScanPosition is empty; add
LastScannedTxnStartTs to the same comparison in the DataRange/TableSpan equality
path so distinct iterator lower bounds no longer compare equal.

Comment on lines +271 to +279
if a.bigTxnMetricState == nil ||
a.bigTxnMetricState.startTs != startTs ||
a.bigTxnMetricState.commitTs != commitTs {
a.bigTxnMetricState = &bigTxnMetricState{
startTs: startTs,
commitTs: commitTs,
largeTxnThresholdInBytes: largeTxnThresholdInBytes,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Flush the pending metric before replacing it.

When a different (startTs, commitTs) arrives, the existing bigTxnMetricState is overwritten without being emitted.

Suggested fix
 	if a.bigTxnMetricState == nil ||
 		a.bigTxnMetricState.startTs != startTs ||
 		a.bigTxnMetricState.commitTs != commitTs {
+		a.finishPendingBigTxnMetricBefore(startTs, commitTs)
 		a.bigTxnMetricState = &bigTxnMetricState{
 			startTs:                  startTs,
 			commitTs:                 commitTs,
 			largeTxnThresholdInBytes: largeTxnThresholdInBytes,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if a.bigTxnMetricState == nil ||
a.bigTxnMetricState.startTs != startTs ||
a.bigTxnMetricState.commitTs != commitTs {
a.bigTxnMetricState = &bigTxnMetricState{
startTs: startTs,
commitTs: commitTs,
largeTxnThresholdInBytes: largeTxnThresholdInBytes,
}
}
if a.bigTxnMetricState == nil ||
a.bigTxnMetricState.startTs != startTs ||
a.bigTxnMetricState.commitTs != commitTs {
a.finishPendingBigTxnMetricBefore(startTs, commitTs)
a.bigTxnMetricState = &bigTxnMetricState{
startTs: startTs,
commitTs: commitTs,
largeTxnThresholdInBytes: largeTxnThresholdInBytes,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/dispatcher_stat.go` around lines 271 - 279, The big
transaction metric state is being replaced in the dispatcher without first
emitting the currently pending metric, which drops data when a new (startTs,
commitTs) pair arrives. Update the logic in the bigTxnMetricState handling block
to flush/emit the existing state before assigning a new bigTxnMetricState, using
the same code path that records the metric so the pending value is not lost.

Comment on lines +30 to +48
func newLargeTxnInsertSpill(dir string) (*largeTxnInsertSpill, error) {
if dir == "" {
return nil, errors.New("large txn spill dir is empty")
}
file, err := recordspill.NewRecordFile(dir, "eventservice-large-txn-insert-*.spill")
if err != nil {
return nil, err
}

return &largeTxnInsertSpill{
path: file.Path(),
file: file,
}, nil
}

func (s *largeTxnInsertSpill) Append(entry *common.RawKVEntry) error {
if entry == nil {
return errors.New("cannot append nil RawKVEntry to large txn spill")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use predefined repository errors instead of ad-hoc errors.New.

newLargeTxnInsertSpill and Append raise generic errors via errors.New(...) rather than a predefined repository error (e.g. reusing cerror.ErrSpillFileOp, which already exists for this exact spill-file domain). As per coding guidelines, "Use predefined repository errors; see docs/agents/error-handling.md before changing error creation, wrapping, or propagation."

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 30-30: : # github.com/pingcap/ticdc/cmd/kafka-consumer [github.com/pingcap/ticdc/cmd/kafka-consumer.test]
cmd/kafka-consumer/consumer.go:30:22: undefined: kafka.ConfigMap
cmd/kafka-consumer/consumer.go:39:22: undefined: kafka.NewAdminClient
cmd/kafka-consumer/consumer.go:57:24: undefined: kafka.Error
cmd/kafka-consumer/consumer.go:58:62: undefined: kafka.ErrTransport
cmd/kafka-consumer/consumer.go:90:16: undefined: kafka.Consumer
cmd/kafka-consumer/writer.go:45:24: undefined: kafka.Offset
cmd/kafka-consumer/writer.go:59:79: undefined: kafka.Offset
cmd/kafka-consumer/writer.go:317:67: undefined: kafka.Message
cmd/kafka-consumer/writer.go:554:84: undefined: kafka.Offset
cmd/kafka-consumer/writer.go:581:97: undefined: kafka.Offset
cmd/kafka-consumer/consumer.go:58:62: too many errors

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/large_txn_spill.go` around lines 30 - 48,
newLargeTxnInsertSpill and largeTxnInsertSpill.Append are creating ad-hoc errors
with errors.New instead of using the repository’s predefined spill-file error.
Update these paths to return the existing spill-related error type (for example
cerror.ErrSpillFileOp) and preserve the specific context for empty dir, record
file creation failure, and nil entry cases. Use the symbols
newLargeTxnInsertSpill, Append, and cerror.ErrSpillFileOp to locate and replace
the generic error creation consistently.

Source: Coding guidelines

Comment on lines +61 to +65
if state.startTs != startTs || state.commitTs != commitTs || state.tableID != tableID {
return nil, errors.Errorf(
"large txn spill state mismatch, existing start-ts: %d, commit-ts: %d, table-id: %d, new start-ts: %d, commit-ts: %d, table-id: %d",
state.startTs, state.commitTs, state.tableID, startTs, commitTs, tableID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use predefined repository errors instead of ad-hoc errors.New/errors.Errorf.

Similar to large_txn_spill.go, this file raises generic errors (state mismatch, cleaned-up state, wrong phase) rather than predefined repository errors. As per coding guidelines, "Use predefined repository errors; see docs/agents/error-handling.md before changing error creation, wrapping, or propagation."

Also applies to: 124-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/eventservice/large_txn_state.go` around lines 61 - 65, The large-txn
state checks in large_txn_state.go are returning ad-hoc errors instead of the
repository’s predefined error types. Update the mismatch/cleanup/wrong-phase
paths in the relevant large txn state helpers (including the state comparison
block and the related cases in the same file) to return the existing shared
errors used by large_txn_spill.go, and follow the error-handling guidance in
docs/agents/error-handling.md so callers can reliably match these conditions.

Source: Coding guidelines

Comment thread pkg/spill/record_file.go
Comment thread pkg/spill/record_file.go
Signed-off-by: dongmen <414110582@qq.com>
@ti-chi-bot

ti-chi-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

@asddongmen: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-error-log-review cb1b3a2 link true /test pull-error-log-review

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eventservice: EventBroker can OOM when syncing large transactions

1 participant