Skip to content

consumer: use dml message instead of dml event - #5590

Merged
ti-chi-bot[bot] merged 14 commits into
pingcap:masterfrom
wk989898:consumer-0706
Jul 21, 2026
Merged

consumer: use dml message instead of dml event#5590
ti-chi-bot[bot] merged 14 commits into
pingcap:masterfrom
wk989898:consumer-0706

Conversation

@wk989898

@wk989898 wk989898 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What problem does this PR solve?

Issue Number: close #5587

What is changed and how it works?

  • DML decoding now returns DMLMessage instead of eagerly building DMLEvent. NextDMLMessage() only exposes metadata such as tableID, schema, table, rowType, and commitTs.

  • DMLEvent construction is deferred until flush time. Event groups store DMLMessage and sort/resolve by commitTs. Only when DML is actually flushed does the consumer call message.ToDMLEvent().

  • Simple protocol cached DML now also uses DMLMessage. If a simple DML arrives before its table info, it is cached as the raw message. After the DDL/table info arrives, the cache releases DMLMessage, not prebuilt DMLEvent.

  • The deferred toDMLEvent callbacks were made safe for delayed/asynchronous use. They no longer restore data into decoder cursor fields such as d.msg, d.keyPayload, or d.valuePayload. Instead, they convert using data captured by the DMLMessage itself.

  • Message payloads are no longer mutated during conversion. Simple and canal-json column formatting now returns new maps instead of modifying the original message maps in place.

  • Shared codec caches were protected. The fake table ID allocator and canal-json table-info/DDL commit-ts caches now use locks so deferred conversion cannot race with later decoder/cache updates.

  • Kafka partition validation is still preserved. messageWithPartitionCheck wraps DMLMessage.ToDMLEvent() so the partition check runs only when the DML is actually converted at flush time.

Check List

Tests

  • Unit test
  • Integration test

Questions

Will it cause performance regression or break compatibility?
Do you need to update user documentation, design documentation or monitoring documentation?

Release note

Please refer to [Release Notes Language Style Guide](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/release-notes-style-guide.html) to write a quality release note.

If you don't think this PR needs a release note then fill it with `None`.

Summary by CodeRabbit

  • New Features

    • Added a message-based DML flow to unify buffering, grouping, and emission across Kafka, Pulsar, and storage pipelines (with assembly deferred until flush for batching consistency).
  • Bug Fixes

    • Improved DML flush correctness by resolving per table/group and computing batch totals consistently.
    • Strengthened routed DML fallback/ignore behavior with stricter partition validation.
    • Pulsar consumer now uses cumulative acknowledgments for more reliable progress tracking.
    • Enhanced DDL-aware table metadata routing by tracking DDL commit timestamps.
  • Refactor

    • Updated decoders and pipelines/tests to use the new DML-message API and convert to row events at the appropriate stage.

wk989898 added 2 commits July 6, 2026 06:31
Signed-off-by: wk989898 <nhsmwk@gmail.com>
Signed-off-by: wk989898 <nhsmwk@gmail.com>
@ti-chi-bot ti-chi-bot Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. do-not-merge/needs-triage-completed release-note Denotes a PR that will be considered when it comes time to generate release notes. labels Jul 6, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 6, 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

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: f83a3703-ec3e-41fe-bd78-ca1e31884127

📥 Commits

Reviewing files that changed from the base of the PR and between 748a923 and 66fc158.

📒 Files selected for processing (1)
  • cmd/pulsar-consumer/writer.go

📝 Walkthrough

Walkthrough

DML decoding now returns deferred DMLMessage objects across sink codecs. Consumers buffer messages in ordered groups and assemble DMLEvent values during watermark flushes. Writer paths, cache handling, partition checks, tests, allocator synchronization, and Pulsar acknowledgements were updated.

Changes

DML message pipeline

Layer / File(s) Summary
Message contract and decoder migration
pkg/sink/codec/common/..., pkg/sink/codec/{avro,canal,csv,debezium,open,simple}/...
Decoders now return DMLMessage values and defer event construction until ToDMLEvent(). Canal caches table metadata by DDL commit timestamp.
Message grouping and flush conversion
cmd/util/event_group.go, cmd/{kafka,pulsar,storage}-consumer/...
Groups store ordered messages, resolve them by watermark, and merge assembled events during flushes with partition and fallback handling.
Validation and compatibility tests
cmd/**/*_test.go, pkg/sink/codec/**/*_test.go
Tests cover deferred assembly, cached DML replay, message grouping, decoder migration, and DDL-aware table-info selection.
Acknowledgement and allocator synchronization
cmd/pulsar-consumer/consumer.go, pkg/sink/codec/common/table_info_cache.go
Pulsar uses cumulative acknowledgements, and table ID allocator state is protected by an RWMutex.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: asddongmen, lidezhu, hongyunyan

Poem

I’m a rabbit with messages tucked in my pack,
Deferred little rows on a watermark track.
Groups hold them gently, then flush them with care,
While timestamps and partitions align in the air.
Hop, hop—the events now assemble just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely describes the main refactor from DMLEvent to DMLMessage.
Description check ✅ Passed The PR description includes the required issue number, change summary, tests, questions, and release note section.
Linked Issues check ✅ Passed The changes address #5587 by deferring DML assembly and aligning schema resolution with DDL commit-ts during flush.
Out of Scope Changes check ✅ Passed All code changes appear tied to the DMLMessage refactor and the schema-timing fix; no unrelated edits stand out.
✨ 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 Jul 6, 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 refactors the consumer and codec components to use a deferred DMLMessage abstraction instead of assembling DMLEvent objects immediately, which optimizes memory usage and performance. Additionally, the Canal-JSON decoder is updated to track DDL commit timestamps for more accurate table schema caching across column changes. The code review feedback suggests several defensive programming enhancements to prevent potential nil pointer dereferences and nil map panics, specifically by checking for nil messages in consumer.go and event_group.go, and ensuring the ddlCommitTs map is properly initialized and checked in canal_json_decoder.go.

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 cmd/storage-consumer/consumer.go Outdated
Comment on lines +354 to +356
row := decoder.NextDMLMessage().ToDMLEvent()
row.PhysicalTableID = tableID
c.appendRow2Group(row, fileIdx.EnableTableAcrossNodes)
c.appendMessage2Group(common.NewDMLMessageFromEvent(row), fileIdx.EnableTableAcrossNodes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

For defensive programming, we should check if decoder.NextDMLMessage() is nil before calling .ToDMLEvent() to avoid any potential nil pointer dereference panic.

			dmlMsg := decoder.NextDMLMessage()
			if dmlMsg == nil {
				log.Panic("DML message is nil, it's not expected")
			}
			row := dmlMsg.ToDMLEvent()
			row.PhysicalTableID = tableID
			c.appendMessage2Group(common.NewDMLMessageFromEvent(row), fileIdx.EnableTableAcrossNodes)
References
  1. Defensive programming: always check for nil pointers before dereferencing or calling methods on objects that can be nil.

Comment on lines +594 to +597
func (d *decoder) addDDLCommitTs(schema, table string, commitTs uint64) {
if schema == "" || table == "" || commitTs == 0 {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If d.ddlCommitTs is not initialized (e.g., if the decoder is created manually in tests or other packages without calling NewDecoder), calling d.ddlCommitTs[key] will panic. We should defensively initialize it if it is nil.

func (d *decoder) addDDLCommitTs(schema, table string, commitTs uint64) {
	if schema == "" || table == "" || commitTs == 0 {
		return
	}
	if d.ddlCommitTs == nil {
		d.ddlCommitTs = make(map[tableNameKey][]uint64)
	}

	key := tableNameKey{schema: schema, table: table}
References
  1. Defensive programming: ensure maps are initialized before writing to them to avoid nil map panics.

Comment on lines +610 to +615
func (d *decoder) getDDLCommitTs(schema, table string, commitTs uint64) uint64 {
if commitTs == 0 {
return 0
}

commitTsList := d.ddlCommitTs[tableNameKey{schema: schema, table: table}]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If d.ddlCommitTs is nil, accessing it will panic. We should defensively check if d.ddlCommitTs is nil before querying it.

Suggested change
func (d *decoder) getDDLCommitTs(schema, table string, commitTs uint64) uint64 {
if commitTs == 0 {
return 0
}
commitTsList := d.ddlCommitTs[tableNameKey{schema: schema, table: table}]
func (d *decoder) getDDLCommitTs(schema, table string, commitTs uint64) uint64 {
if commitTs == 0 || d.ddlCommitTs == nil {
return 0
}
commitTsList := d.ddlCommitTs[tableNameKey{schema: schema, table: table}]
References
  1. Defensive programming: check for nil maps before reading from them to avoid nil pointer panics.

Comment thread cmd/util/event_group.go
Comment on lines +107 to +113
func AppendOrMergeDMLEvent(events []*commonEvent.DMLEvent, row *commonEvent.DMLEvent) []*commonEvent.DMLEvent {
var lastDMLEvent *commonEvent.DMLEvent
if len(events) > 0 {
lastDMLEvent = events[len(events)-1]
}

if lastDMLEvent == nil || lastDMLEvent.GetCommitTs() < row.GetCommitTs() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If row is nil, calling row.GetCommitTs() will panic with a nil pointer dereference. We should defensively check if row is nil at the beginning of AppendOrMergeDMLEvent.

func AppendOrMergeDMLEvent(events []*commonEvent.DMLEvent, row *commonEvent.DMLEvent) []*commonEvent.DMLEvent {
	if row == nil {
		return events
	}
	var lastDMLEvent *commonEvent.DMLEvent
	if len(events) > 0 {
		lastDMLEvent = events[len(events)-1]
	}

	if lastDMLEvent == nil || lastDMLEvent.GetCommitTs() < row.GetCommitTs() {
References
  1. Defensive programming: check for nil arguments before calling methods on them to avoid nil pointer panics.

Signed-off-by: wk989898 <nhsmwk@gmail.com>
@wk989898

wk989898 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

/test kafka
/test pulsar

@wk989898

wk989898 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

/test kafka

wk989898 added 3 commits July 7, 2026 09:31
Signed-off-by: wk989898 <nhsmwk@gmail.com>
Signed-off-by: wk989898 <nhsmwk@gmail.com>
Signed-off-by: wk989898 <nhsmwk@gmail.com>
@wk989898
wk989898 marked this pull request as ready for review July 20, 2026 04:19
@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 20, 2026
@wk989898

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: 1

Caution

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

⚠️ Outside diff range comments (1)
pkg/sink/codec/open/decoder.go (1)

195-271: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Open decoder decompresses and decodes each DML value twice per message.

NextDMLMessage calls rowTypeFromDMLValue(value) (Line 207) which decompresses and decode()s the payload just to classify RowType. The deferred closure then calls decodeDMLMessage(&key, value) (Line 211), which decompresses and decodes the same bytes again from scratch to build the event. Unlike the avro/canal-json decoders in this PR, which decode once up front and reuse the parsed structures inside the deferred closure, this decoder pays for decompression + deserialization twice for every non-claim-check row on the hot decode path.

Consider caching the decoded *messageRow from rowTypeFromDMLValue and passing it directly into the closure instead of re-decoding:

⚡ Illustrative fix: decode the row once and reuse it
 func (b *decoder) NextDMLMessage() *common.DMLMessage {
 	if b.nextKey.Type != common.MessageTypeRow {
 		log.Panic("message type is not row", zap.Any("messageType", b.nextKey.Type))
 	}
 
 	key := *b.nextKey
 	value := b.nextDMLValue()
 	b.nextKey = nil
 
 	rowType := commonType.RowTypeInsert
+	var decodedRow *messageRow
 	if key.ClaimCheckLocation == "" {
-		rowType = b.rowTypeFromDMLValue(value)
+		decodedRow = b.decodeDMLValue(value)
+		rowType = rowTypeFromMessageRow(decodedRow)
 	}
 	tableID := tableIDAllocator.Allocate(key.Schema, key.Table)
 	return common.NewDMLMessage(tableID, key.Schema, key.Table, key.Ts, rowType, func() *commonEvent.DMLEvent {
-		return b.decodeDMLMessage(&key, value)
+		return b.decodeDMLMessageWithRow(&key, decodedRow, value)
 	})
 }

-func (b *decoder) rowTypeFromDMLValue(value []byte) commonType.RowType {
-	value, err := common.Decompress(b.config.LargeMessageHandle.LargeMessageHandleCompression, value)
-	...
-	nextRow := new(messageRow)
-	nextRow.decode(value)
-	return rowTypeFromMessageRow(nextRow)
-}
+func (b *decoder) decodeDMLValue(value []byte) *messageRow {
+	value, err := common.Decompress(b.config.LargeMessageHandle.LargeMessageHandleCompression, value)
+	if err != nil {
+		log.Panic("decompress failed", ...)
+	}
+	row := new(messageRow)
+	row.decode(value)
+	return row
+}

-func (b *decoder) decodeDMLMessage(key *messageKey, value []byte) *commonEvent.DMLEvent {
-	value, err := common.Decompress(b.config.LargeMessageHandle.LargeMessageHandleCompression, value)
-	...
-	nextRow := new(messageRow)
-	nextRow.decode(value)
-
-	ctx := context.Background()
+func (b *decoder) decodeDMLMessageWithRow(key *messageKey, nextRow *messageRow, value []byte) *commonEvent.DMLEvent {
+	ctx := context.Background()
 	if key.ClaimCheckLocation != "" {
 		return b.assembleEventFromClaimCheckStorage(ctx, key)
 	}
 	if key.OnlyHandleKey && b.upstreamTiDB != nil {
 		return b.assembleHandleKeyOnlyDMLEvent(ctx, key, nextRow)
 	}
 	return b.assembleDMLEvent(key, nextRow)
 }

(claim-check messages already skip the eager decode today, so value/decode-on-demand can stay for that branch only)

🤖 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/sink/codec/open/decoder.go` around lines 195 - 271, Update
NextDMLMessage, rowTypeFromDMLValue, and decodeDMLMessage to decode
non-claim-check DML payloads only once: cache the parsed *messageRow used to
determine rowType and pass it into the deferred event-building path. Preserve
claim-check handling by skipping eager value decoding and retaining its existing
on-demand behavior.
🧹 Nitpick comments (2)
cmd/util/event_group.go (1)

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

Manual slice-insert works correctly.

The append-nil/copy/shift-right pattern here is a correct manual re-implementation of slices.Insert. Not a bug, just a minor style note: the standard-library slices.Insert(g.messages, i, message) would be more concise and equally correct if the slices dependency removal wasn't intentional.

🤖 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 `@cmd/util/event_group.go` around lines 60 - 68, No code change is required:
the manual insertion in the force branch of the surrounding event-group method
is correct. Optionally replace the append/copy/assignment sequence with
slices.Insert using the computed index, but only if restoring or retaining the
slices dependency is intentional.
pkg/sink/codec/open/decoder.go (1)

337-339: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Deferred closure mutates the captured *messageKey (key.OnlyHandleKey = false).

assembleHandleKeyOnlyDMLEvent mutates key (the same pointer captured by NextDMLMessage's closure via &key). This is safe as long as ToDMLEvent() is invoked exactly once per message; if it's ever invoked twice (retry, re-flush, etc.), the second call would skip the handle-key-only branch it already skipped, producing a different/incorrect result silently. No current caller appears to invoke it twice, so this is speculative, but worth a defensive note given the whole design leans on closures capturing mutable pointers.

🤖 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/sink/codec/open/decoder.go` around lines 337 - 339, Remove the mutation
of the captured messageKey in assembleHandleKeyOnlyDMLEvent by avoiding changes
to key.OnlyHandleKey; preserve the method’s handle-key-only assembly behavior
while ensuring repeated ToDMLEvent invocations produce the same result.
🤖 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/sink/codec/debezium/decoder.go`:
- Around line 172-173: The Debezium decoder must eagerly register allocated
table IDs before deferred DML event assembly. In
pkg/sink/codec/debezium/decoder.go:172-173, update the allocation flow around
Allocate to immediately call AddBlockTableID, and remove the duplicate
registration in queryTableInfoFromPayload. Apply the same immediate registration
after Allocate in pkg/sink/codec/canal/canal_json_txn_decoder.go:111-114 so both
decoders preserve accurate blocked-table partition mapping.

---

Outside diff comments:
In `@pkg/sink/codec/open/decoder.go`:
- Around line 195-271: Update NextDMLMessage, rowTypeFromDMLValue, and
decodeDMLMessage to decode non-claim-check DML payloads only once: cache the
parsed *messageRow used to determine rowType and pass it into the deferred
event-building path. Preserve claim-check handling by skipping eager value
decoding and retaining its existing on-demand behavior.

---

Nitpick comments:
In `@cmd/util/event_group.go`:
- Around line 60-68: No code change is required: the manual insertion in the
force branch of the surrounding event-group method is correct. Optionally
replace the append/copy/assignment sequence with slices.Insert using the
computed index, but only if restoring or retaining the slices dependency is
intentional.

In `@pkg/sink/codec/open/decoder.go`:
- Around line 337-339: Remove the mutation of the captured messageKey in
assembleHandleKeyOnlyDMLEvent by avoiding changes to key.OnlyHandleKey; preserve
the method’s handle-key-only assembly behavior while ensuring repeated
ToDMLEvent invocations produce the same result.
🪄 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: 75d0c5be-ec56-4822-be7f-74da0c52d7e1

📥 Commits

Reviewing files that changed from the base of the PR and between 2258270 and 2ec9c3a.

📒 Files selected for processing (28)
  • cmd/kafka-consumer/writer.go
  • cmd/kafka-consumer/writer_test.go
  • cmd/pulsar-consumer/consumer.go
  • cmd/pulsar-consumer/writer.go
  • cmd/pulsar-consumer/writer_test.go
  • cmd/storage-consumer/consumer.go
  • cmd/util/event_group.go
  • cmd/util/event_group_test.go
  • pkg/sink/codec/avro/avro_test.go
  • pkg/sink/codec/avro/decoder.go
  • pkg/sink/codec/avro/encoder_test.go
  • pkg/sink/codec/canal/canal_json_decoder.go
  • pkg/sink/codec/canal/canal_json_encoder_test.go
  • pkg/sink/codec/canal/canal_json_test.go
  • pkg/sink/codec/canal/canal_json_txn_decoder.go
  • pkg/sink/codec/common/decoder.go
  • pkg/sink/codec/common/table_info_cache.go
  • pkg/sink/codec/csv/csv_decoder.go
  • pkg/sink/codec/csv/csv_decoder_test.go
  • pkg/sink/codec/debezium/avro_decoder.go
  • pkg/sink/codec/debezium/avro_test.go
  • pkg/sink/codec/debezium/debezium_test.go
  • pkg/sink/codec/debezium/decoder.go
  • pkg/sink/codec/open/decoder.go
  • pkg/sink/codec/open/encoder_test.go
  • pkg/sink/codec/simple/decoder.go
  • pkg/sink/codec/simple/decoder_test.go
  • pkg/sink/codec/simple/encoder_test.go

Comment on lines +172 to +173
tableID := tableIDAllocator.Allocate(schemaName, tableName)
d.clear()

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.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Eagerly register allocated table IDs to prevent out-of-order DDL/DML execution.

Both decoders allocate a table ID for the incoming DML message but fail to eagerly register it with the allocator's blocked list. Because event assembly (ToDMLEvent) is now deferred, delaying or omitting AddBlockTableID means an interleaving DDL event will not include these physical table IDs in its ddl.GetBlockedTables() list. Consequently, the consumer will mistakenly skip the pre-DDL flush for these buffered DMLs, violating execution order and leading to schema errors (such as the "Unknown column" downstream failure).

  • pkg/sink/codec/debezium/decoder.go#L172-L173: Add tableIDAllocator.AddBlockTableID(schemaName, tableName, tableID) immediately after allocating the tableID. (You can safely remove the duplicate registration inside queryTableInfoFromPayload since it's now covered eagerly).
  • pkg/sink/codec/canal/canal_json_txn_decoder.go#L111-L114: Add tableIDAllocator.AddBlockTableID(schemaName, tableName, tableID) immediately after allocating the tableID here as well to preserve accurate partition mapping on flush.
📍 Affects 2 files
  • pkg/sink/codec/debezium/decoder.go#L172-L173 (this comment)
  • pkg/sink/codec/canal/canal_json_txn_decoder.go#L111-L114
🤖 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/sink/codec/debezium/decoder.go` around lines 172 - 173, The Debezium
decoder must eagerly register allocated table IDs before deferred DML event
assembly. In pkg/sink/codec/debezium/decoder.go:172-173, update the allocation
flow around Allocate to immediately call AddBlockTableID, and remove the
duplicate registration in queryTableInfoFromPayload. Apply the same immediate
registration after Allocate in
pkg/sink/codec/canal/canal_json_txn_decoder.go:111-114 so both decoders preserve
accurate blocked-table partition mapping.

Signed-off-by: wk989898 <nhsmwk@gmail.com>
@wk989898

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: 1

Caution

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

⚠️ Outside diff range comments (2)
pkg/sink/codec/avro/decoder.go (2)

269-284: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent native panics on missing connect.parameters or tidb_type.

If connect.parameters or tidb_type is absent in the decoded Avro schema (e.g., if the schema was not generated with TiDB extensions), type assertions like .(map[string]any) or .(string) will fail on nil interfaces. This causes a native Go panic, bypassing standard error handling and abruptly crashing the consumer when processing malformed or unexpected external schemas.

Safely assert these types and return an error instead.

🛡️ Proposed fix to add safe type assertions
 		var holder map[string]any
 		switch ty := field["type"].(type) {
 		case []any:
 			if m, ok := ty[0].(map[string]any); ok {
-				holder = m["connect.parameters"].(map[string]any)
+				if params, ok := m["connect.parameters"].(map[string]any); ok {
+					holder = params
+				}
 			} else if m, ok := ty[1].(map[string]any); ok {
-				holder = m["connect.parameters"].(map[string]any)
+				if params, ok := m["connect.parameters"].(map[string]any); ok {
+					holder = params
+				}
 			} else {
 				log.Panic("type info is anything else", zap.Any("typeInfo", field["type"]))
 			}
 		case map[string]any:
-			holder = ty["connect.parameters"].(map[string]any)
+			if params, ok := ty["connect.parameters"].(map[string]any); ok {
+				holder = params
+			}
 		default:
 			log.Panic("type info is anything else", zap.Any("typeInfo", field["type"]))
 		}
+
+		if holder == nil {
+			return nil, errors.ErrCodecDecode.GenWithStack("connect.parameters not found or invalid in schema field")
+		}
 
-		tidbType := holder["tidb_type"].(string)
+		tidbType, ok := holder["tidb_type"].(string)
+		if !ok {
+			return nil, errors.ErrCodecDecode.GenWithStack("tidb_type not found or invalid in connect.parameters")
+		}
🤖 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/sink/codec/avro/decoder.go` around lines 269 - 284, Update the schema
type handling around the field decoder’s holder extraction and tidbType
assignment to safely validate connect.parameters as map[string]any and tidb_type
as string instead of using unchecked assertions. On missing or invalid values,
return the decoder’s standard error rather than triggering a native panic;
preserve the existing handling for valid map and array schema forms.

307-307: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent out-of-bounds panic on namespace split.

If the Avro schema's namespace does not contain a dot (e.g., "myschema" instead of "myschema.mytable"), strings.Split(namespace, ".")[1] will panic with an index out-of-bounds error. Additionally, if namespace or name is absent, the type assertion .(string) will also trigger a native panic, crashing the consumer.

Safely assert these types, validate the namespace format, and return an error instead of crashing.

🛠️ Proposed fix to handle namespace extraction safely

Update the function to return an error safely:

-func schemaAndTableName(schema map[string]any) (string, string) {
-	namespace := schema["namespace"].(string)
-	return strings.Split(namespace, ".")[1], schema["name"].(string)
-}
+func schemaAndTableName(schema map[string]any) (string, string, error) {
+	ns, ok := schema["namespace"].(string)
+	if !ok {
+		return "", "", errors.ErrCodecDecode.GenWithStack("schema namespace must be a string")
+	}
+	parts := strings.Split(ns, ".")
+	if len(parts) < 2 {
+		return "", "", errors.ErrCodecDecode.GenWithStack("schema namespace must contain a dot, got: %s", ns)
+	}
+	name, ok := schema["name"].(string)
+	if !ok {
+		return "", "", errors.ErrCodecDecode.GenWithStack("schema name must be a string")
+	}
+	return parts[1], name, nil
+}

And handle the error in assembleEvent:

-	schemaName, tableName := schemaAndTableName(schema)
+	schemaName, tableName, err := schemaAndTableName(schema)
+	if err != nil {
+		return nil, err
+	}

Also applies to: 338-341

🤖 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/sink/codec/avro/decoder.go` at line 307, Update schemaAndTableName to
safely validate namespace and name type assertions, require a dotted namespace
before extracting schema and table components, and return an error for missing
or malformed values instead of panicking. Update assembleEvent to handle and
propagate the returned error from schemaAndTableName.
🤖 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/sink/codec/avro/decoder.go`:
- Around line 249-252: In pkg/sink/codec/avro/decoder.go, replace the four
errors.New calls in the schema field parsing and value/commit timestamp lookup
paths with errors.ErrCodecDecode.GenWithStack: use “schema fields should be an
array” for the fields assertion at lines 249-252, retain “schema field should be
a map” at lines 258-261, include colName in “value not found for column: %s” at
lines 288-290, and use “commit ts not found” at lines 312-314.

---

Outside diff comments:
In `@pkg/sink/codec/avro/decoder.go`:
- Around line 269-284: Update the schema type handling around the field
decoder’s holder extraction and tidbType assignment to safely validate
connect.parameters as map[string]any and tidb_type as string instead of using
unchecked assertions. On missing or invalid values, return the decoder’s
standard error rather than triggering a native panic; preserve the existing
handling for valid map and array schema forms.
- Line 307: Update schemaAndTableName to safely validate namespace and name type
assertions, require a dotted namespace before extracting schema and table
components, and return an error for missing or malformed values instead of
panicking. Update assembleEvent to handle and propagate the returned error from
schemaAndTableName.
🪄 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: 631ee8d7-f08b-4163-ab38-ef5a4eca28a3

📥 Commits

Reviewing files that changed from the base of the PR and between 899f820 and 6dfbcc7.

📒 Files selected for processing (5)
  • cmd/kafka-consumer/writer.go
  • cmd/pulsar-consumer/writer.go
  • cmd/storage-consumer/consumer.go
  • pkg/sink/codec/avro/decoder.go
  • pkg/sink/codec/open/encoder_test.go
💤 Files with no reviewable changes (1)
  • pkg/sink/codec/open/encoder_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • cmd/storage-consumer/consumer.go
  • cmd/kafka-consumer/writer.go
  • cmd/pulsar-consumer/writer.go

Comment on lines +249 to 252
fields, ok := schema["fields"].([]any)
if !ok {
return nil, errors.New("schema fields should be a map")
}

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 errors.New.

The use of errors.New violates the coding guidelines that require using predefined repository errors for error creation. Additionally, the error message for the fields type assertion inaccurately expects a "map" when checking for a slice ([]any). Use errors.ErrCodecDecode.GenWithStack consistently as seen elsewhere in this file.

  • pkg/sink/codec/avro/decoder.go#L249-L252: replace errors.New("schema fields should be a map") with errors.ErrCodecDecode.GenWithStack("schema fields should be an array").
  • pkg/sink/codec/avro/decoder.go#L258-L261: replace errors.New("schema field should be a map") with errors.ErrCodecDecode.GenWithStack("schema field should be a map").
  • pkg/sink/codec/avro/decoder.go#L288-L290: replace errors.New("value not found") with errors.ErrCodecDecode.GenWithStack("value not found for column: %s", colName).
  • pkg/sink/codec/avro/decoder.go#L312-L314: replace errors.New("commit ts not found") with errors.ErrCodecDecode.GenWithStack("commit ts not found").
📍 Affects 1 file
  • pkg/sink/codec/avro/decoder.go#L249-L252 (this comment)
  • pkg/sink/codec/avro/decoder.go#L258-L261
  • pkg/sink/codec/avro/decoder.go#L288-L290
  • pkg/sink/codec/avro/decoder.go#L312-L314
🤖 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/sink/codec/avro/decoder.go` around lines 249 - 252, In
pkg/sink/codec/avro/decoder.go, replace the four errors.New calls in the schema
field parsing and value/commit timestamp lookup paths with
errors.ErrCodecDecode.GenWithStack: use “schema fields should be an array” for
the fields assertion at lines 249-252, retain “schema field should be a map” at
lines 258-261, include colName in “value not found for column: %s” at lines
288-290, and use “commit ts not found” at lines 312-314.

Source: Coding guidelines

wk989898 added 3 commits July 20, 2026 08:52
Signed-off-by: wk989898 <nhsmwk@gmail.com>
Signed-off-by: wk989898 <nhsmwk@gmail.com>
.
Signed-off-by: wk989898 <nhsmwk@gmail.com>

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
pkg/sink/codec/debezium/decoder.go (1)

278-288: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Move blocked-table registration into the decode path
tableIDAllocator.Allocate(...) already happens in NextDMLMessage; only AddBlockTableID(...) is deferred here through queryTableInfoFromPayload. Register the table ID before clearing decoder state so interleaved DDLs don’t miss this table in GetBlockedTables().

🤖 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/sink/codec/debezium/decoder.go` around lines 278 - 288, Move the
AddBlockTableID registration from queryTableInfoFromPayload into the decode path
in NextDMLMessage, immediately after Allocate and before decoder state is
cleared. Remove the deferred registration from queryTableInfoFromPayload while
preserving the existing table ID and table metadata construction.
🧹 Nitpick comments (2)
pkg/sink/codec/debezium/decoder.go (2)

336-337: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Preallocate the map with the expected capacity.

Since the maximum number of columns is known, preallocating the map can reduce reallocations during assembly.

⚡ Proposed fix
 func assembleColumnData(data map[string]any, columns []*timodel.ColumnInfo, timeZone *time.Location) map[string]any {
-	result := make(map[string]any, 0)
+	result := make(map[string]any, len(columns))
🤖 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/sink/codec/debezium/decoder.go` around lines 336 - 337, Update
assembleColumnData to initialize its result map with capacity len(columns),
using the known column count instead of zero while preserving the existing
assembly behavior.

336-337: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Preallocate the map with the expected capacity.

Since the maximum number of columns is known, preallocating the map can reduce unnecessary memory allocations and rehashing during column data assembly.

⚡ Proposed fix
 func assembleColumnData(data map[string]any, columns []*timodel.ColumnInfo, timeZone *time.Location) map[string]any {
-	result := make(map[string]any, 0)
+	result := make(map[string]any, len(columns))
🤖 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/sink/codec/debezium/decoder.go` around lines 336 - 337, Update
assembleColumnData to initialize its result map with columns’ expected capacity
rather than zero, using len(columns) as the map size hint while preserving the
existing assembly behavior.
🤖 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/sink/codec/debezium/decoder.go`:
- Around line 527-530: Update the error returned in the schema validation block
of the decoder to say “decode schema failed” instead of “decode payload failed,”
while preserving the existing data context and control flow.
- Around line 527-530: Update the error returned by the schema type assertion in
the decoder flow to say “decode schema failed” instead of “decode payload
failed,” while preserving the existing data context and control flow.

---

Outside diff comments:
In `@pkg/sink/codec/debezium/decoder.go`:
- Around line 278-288: Move the AddBlockTableID registration from
queryTableInfoFromPayload into the decode path in NextDMLMessage, immediately
after Allocate and before decoder state is cleared. Remove the deferred
registration from queryTableInfoFromPayload while preserving the existing table
ID and table metadata construction.

---

Nitpick comments:
In `@pkg/sink/codec/debezium/decoder.go`:
- Around line 336-337: Update assembleColumnData to initialize its result map
with capacity len(columns), using the known column count instead of zero while
preserving the existing assembly behavior.
- Around line 336-337: Update assembleColumnData to initialize its result map
with columns’ expected capacity rather than zero, using len(columns) as the map
size hint while preserving the existing assembly behavior.
🪄 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: 944278d2-ba93-4ebc-8167-e4e6d9f99517

📥 Commits

Reviewing files that changed from the base of the PR and between 6dfbcc7 and 748a923.

📒 Files selected for processing (3)
  • pkg/sink/codec/canal/canal_json_test.go
  • pkg/sink/codec/debezium/decoder.go
  • pkg/sink/codec/simple/decoder_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/sink/codec/simple/decoder_test.go
  • pkg/sink/codec/canal/canal_json_test.go

Comment on lines +527 to 530
schema, ok := v["schema"].(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("decode payload failed, data: %+v", v)
}

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 | 🟡 Minor | ⚡ Quick win

Fix the error message to accurately reflect the failure.

The error message indicates that decoding the payload failed, but this block is actually validating the schema. Update the message to "decode schema failed" to avoid confusion during debugging.

✏️ Proposed fix
 	schema, ok := v["schema"].(map[string]any)
 	if !ok {
-		return nil, nil, fmt.Errorf("decode payload failed, data: %+v", v)
+		return nil, nil, fmt.Errorf("decode schema failed, data: %+v", v)
 	}
📝 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
schema, ok := v["schema"].(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("decode payload failed, data: %+v", v)
}
schema, ok := v["schema"].(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("decode schema failed, data: %+v", v)
}
🤖 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/sink/codec/debezium/decoder.go` around lines 527 - 530, Update the error
returned in the schema validation block of the decoder to say “decode schema
failed” instead of “decode payload failed,” while preserving the existing data
context and control flow.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the error message to accurately reflect the failure.

The error message indicates that decoding the payload failed, but this block is actually validating the schema. Update the message to "decode schema failed" to avoid confusion during debugging.

✏️ Proposed fix
 	schema, ok := v["schema"].(map[string]any)
 	if !ok {
-		return nil, nil, fmt.Errorf("decode payload failed, data: %+v", v)
+		return nil, nil, fmt.Errorf("decode schema failed, data: %+v", v)
 	}
📝 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
schema, ok := v["schema"].(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("decode payload failed, data: %+v", v)
}
schema, ok := v["schema"].(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("decode schema failed, data: %+v", v)
}
🤖 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/sink/codec/debezium/decoder.go` around lines 527 - 530, Update the error
returned by the schema type assertion in the decoder flow to say “decode schema
failed” instead of “decode payload failed,” while preserving the existing data
context and control flow.

@wk989898

Copy link
Copy Markdown
Collaborator Author

/test all

@ti-chi-bot

ti-chi-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

@wk989898: 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-cdc-pulsar-integration-heavy-next-gen 39d3a6a link false /test pull-cdc-pulsar-integration-heavy-next-gen

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.

@ti-chi-bot ti-chi-bot Bot added needs-1-more-lgtm Indicates a PR needs 1 more LGTM. approved labels Jul 21, 2026
@ti-chi-bot ti-chi-bot Bot added the lgtm label Jul 21, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: 3AceShowHand, lidezhu

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

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [3AceShowHand,lidezhu]

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

@ti-chi-bot ti-chi-bot Bot removed the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Jul 21, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

[LGTM Timeline notifier]

Timeline:

  • 2026-07-21 02:49:33.114913822 +0000 UTC m=+1286759.151008879: ☑️ agreed by 3AceShowHand.
  • 2026-07-21 03:19:24.183362513 +0000 UTC m=+1288550.219457589: ☑️ agreed by lidezhu.

@wk989898 wk989898 added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 21, 2026
Signed-off-by: wk989898 <nhsmwk@gmail.com>
@wk989898 wk989898 removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 21, 2026
@ti-chi-bot
ti-chi-bot Bot merged commit 5573f01 into pingcap:master Jul 21, 2026
10 of 11 checks passed
@wk989898

Copy link
Copy Markdown
Collaborator Author

/cherry-pick release-nextgen-202603

@wk989898 wk989898 added the needs-cherry-pick-release-8.5 Should cherry pick this PR to release-8.5 branch. label Jul 27, 2026
@ti-chi-bot

Copy link
Copy Markdown
Member

@wk989898: new pull request created to branch release-nextgen-202603: #5777.
But this PR has conflicts, please resolve them!

Details

In response to this:

/cherry-pick release-nextgen-202603

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 ti-community-infra/tichi repository.

@ti-chi-bot

Copy link
Copy Markdown
Member

In response to a cherrypick label: new pull request created to branch release-8.5: #5778.
But this PR has conflicts, please resolve them!

wk989898 added a commit that referenced this pull request Jul 27, 2026
* This is an automated cherry-pick of #5590

Signed-off-by: ti-chi-bot <ti-community-prow-bot@tidb.io>

* update

Signed-off-by: wk989898 <nhsmwk@gmail.com>

* update

Signed-off-by: wk989898 <nhsmwk@gmail.com>

* fix

Signed-off-by: wk989898 <nhsmwk@gmail.com>

---------

Signed-off-by: ti-chi-bot <ti-community-prow-bot@tidb.io>
Signed-off-by: wk989898 <nhsmwk@gmail.com>
Co-authored-by: nhsmw <nhsmwk@gmail.com>
ti-chi-bot Bot pushed a commit that referenced this pull request Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved lgtm needs-cherry-pick-release-8.5 Should cherry pick this PR to release-8.5 branch. 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.

unstable test ddl_for_split_tables_with_failover

4 participants