Repository: pingcap/ticdc
Pull request #1350: dispatcher: remove GetStartTsList from sink interface, instead be a special function of mysql sink
Author: login: hongyunyan
Labels: name: lgtm
name: approved
name: release-note
description: Denotes a PR that will be considered when it comes time to generate release notes.
name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
Created: 2025-05-21T08:42:18Z | Merged: 2025-05-22T02:56:34Z

<!--
Thank you for contributing to TiCDC! 
Please read MD's [CONTRIBUTING](https://github.com/pingcap/ticdc/blob/master/CONTRIBUTING.md) document **BEFORE** filing this PR.
-->

### What problem does this PR solve?
<!--
Please create an issue first to describe the problem.

There MUST be one line starting with "Issue Number:  " and 
linking the relevant issues via the "close" or "ref".

For more info, check https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/contribute-code.html#referring-to-an-issue.
 -->

Issue Number: close #xxx

### What is changed and how it works?

### Check List <!--REMOVE the items that are not applicable-->

#### Tests <!-- At least one of them must be included. -->

- Unit test
- Integration test
- Manual test (add detailed scripts or steps below)
- No code

#### Questions <!-- Authors should answer these questions and reviewers should consider these questions. -->

##### Will it cause performance regression or break compatibility?

##### Do you need to update user documentation, design documentation or monitoring documentation?

### Release note <!-- bugfixes or new features need a release note -->

```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`.
```



Diff:
diff --git a/downstreamadapter/dispatcher/dispatcher_test.go b/downstreamadapter/dispatcher/dispatcher_test.go
index 7ad5d61f0e..a9191d1e44 100644
--- a/downstreamadapter/dispatcher/dispatcher_test.go
+++ b/downstreamadapter/dispatcher/dispatcher_test.go
@@ -50,10 +50,6 @@ func (s *mockSink) AddCheckpointTs(_ uint64) {
 func (s *mockSink) SetTableSchemaStore(_ *sinkutil.TableSchemaStore) {
 }
 
-func (s *mockSink) GetStartTsList(_ []int64, startTsList []int64, _ bool) ([]int64, []bool, error) {
-	return startTsList, make([]bool, len(startTsList)), nil
-}
-
 func (s *mockSink) Close(bool) {}
 
 func (s *mockSink) Run(context.Context) error {
diff --git a/downstreamadapter/dispatchermanager/event_dispatcher_manager.go b/downstreamadapter/dispatchermanager/event_dispatcher_manager.go
index 9cb47a1b82..0e64841bf0 100644
--- a/downstreamadapter/dispatchermanager/event_dispatcher_manager.go
+++ b/downstreamadapter/dispatchermanager/event_dispatcher_manager.go
@@ -25,6 +25,7 @@ import (
 	"github.com/pingcap/ticdc/downstreamadapter/dispatcher"
 	"github.com/pingcap/ticdc/downstreamadapter/eventcollector"
 	"github.com/pingcap/ticdc/downstreamadapter/sink"
+	"github.com/pingcap/ticdc/downstreamadapter/sink/mysql"
 	"github.com/pingcap/ticdc/downstreamadapter/syncpoint"
 	"github.com/pingcap/ticdc/eventpb"
 	"github.com/pingcap/ticdc/heartbeatpb"
@@ -475,10 +476,30 @@ func (e *EventDispatcherManager) newDispatchers(infos []dispatcherCreateInfo, re
 	// so we just return the startTs we get.
 	// Besides, we batch the creation for the dispatchers,
 	// mainly because we need to batch the query for startTs when sink is mysql-class to reduce the time cost.
-	newStartTsList, startTsIsSyncpointList, err := e.sink.GetStartTsList(tableIds, startTsList, removeDDLTs)
-	if err != nil {
-		return errors.Trace(err)
+	//
+	// When we enable syncpoint, we also need to know the last ddl commitTs whether is a syncpoint event.
+	// because the commitTs of a syncpoint event can be the same as a ddl event
+	// If there is a ddl event and a syncpoint event at the same time, we ensure the syncpoint event always after the ddl event.
+	// So we need to know whether the commitTs is from a syncpoint event or a ddl event,
+	// to decide whether we need to send generate the syncpoint event of this commitTs to downstream.
+	var newStartTsList []int64
+	startTsIsSyncpointList := make([]bool, len(startTsList))
+	var err error
+	if e.sink.SinkType() == common.MysqlSinkType {
+		newStartTsList, startTsIsSyncpointList, err = e.sink.(*mysql.Sink).GetStartTsList(tableIds, startTsList, removeDDLTs)
+		if err != nil {
+			return errors.Trace(err)
+		}
+		log.Info("calculate real startTs for dispatchers",
+			zap.Stringer("changefeedID", e.changefeedID),
+			zap.Any("receiveStartTs", startTsList),
+			zap.Any("realStartTs", newStartTsList),
+			zap.Bool("removeDDLTs", removeDDLTs),
+		)
+	} else {
+		newStartTsList = startTsList
 	}
+
 	if e.latestWatermark.Get().CheckpointTs == 0 {
 		// If the checkpointTs is 0, means there is no dispatchers before. So we need to init it with the smallest startTs of these dispatchers
 		smallestStartTs := int64(math.MaxInt64)
diff --git a/downstreamadapter/sink/blackhole/sink.go b/downstreamadapter/sink/blackhole/sink.go
index 1ff7653520..d7b8039fdc 100644
--- a/downstreamadapter/sink/blackhole/sink.go
+++ b/downstreamadapter/sink/blackhole/sink.go
@@ -68,10 +68,6 @@ func (s *sink) WriteBlockEvent(event commonEvent.BlockEvent) error {
 func (s *sink) AddCheckpointTs(_ uint64) {
 }
 
-func (s *sink) GetStartTsList(_ []int64, startTsList []int64, _ bool) ([]int64, []bool, error) {
-	return startTsList, make([]bool, len(startTsList)), nil
-}
-
 func (s *sink) Close(_ bool) {}
 
 func (s *sink) Run(_ context.Context) error {
diff --git a/downstreamadapter/sink/cloudstorage/sink.go b/downstreamadapter/sink/cloudstorage/sink.go
index 7270188a38..6dcf0eb06f 100644
--- a/downstreamadapter/sink/cloudstorage/sink.go
+++ b/downstreamadapter/sink/cloudstorage/sink.go
@@ -295,10 +295,6 @@ func (s *sink) SetTableSchemaStore(tableSchemaStore *util.TableSchemaStore) {
 	s.tableSchemaStore = tableSchemaStore
 }
 
-func (s *sink) GetStartTsList(_ []int64, startTsList []int64, _ bool) ([]int64, []bool, error) {
-	return startTsList, make([]bool, len(startTsList)), nil
-}
-
 func (s *sink) initCron(
 	ctx context.Context, sinkURI *url.URL, cleanupJobs []func(),
 ) (err error) {
diff --git a/downstreamadapter/sink/kafka/sink.go b/downstreamadapter/sink/kafka/sink.go
index 57176ea3a9..bbabc7ac8e 100644
--- a/downstreamadapter/sink/kafka/sink.go
+++ b/downstreamadapter/sink/kafka/sink.go
@@ -536,10 +536,6 @@ func (s *sink) SetTableSchemaStore(tableSchemaStore *util.TableSchemaStore) {
 	s.tableSchemaStore = tableSchemaStore
 }
 
-func (s *sink) GetStartTsList(_ []int64, startTsList []int64, _ bool) ([]int64, []bool, error) {
-	return startTsList, make([]bool, len(startTsList)), nil
-}
-
 func (s *sink) getAllTableNames(ts uint64) []*commonEvent.SchemaTableName {
 	if s.tableSchemaStore == nil {
 		log.Warn("kafka sink table schema store is not set",
diff --git a/downstreamadapter/sink/mysql/sink.go b/downstreamadapter/sink/mysql/sink.go
index fd448bfde8..246306db40 100644
--- a/downstreamadapter/sink/mysql/sink.go
+++ b/downstreamadapter/sink/mysql/sink.go
@@ -39,9 +39,9 @@ const (
 	defaultConflictDetectorSlots uint64 = 16 * 1024
 )
 
-// sink is responsible for writing data to mysql downstream.
+// Sink is responsible for writing data to mysql downstream.
 // Including DDL and DML.
-type sink struct {
+type Sink struct {
 	changefeedID common.ChangeFeedID
 
 	dmlWriter []*mysql.Writer
@@ -78,7 +78,7 @@ func New(
 	changefeedID common.ChangeFeedID,
 	config *config.ChangefeedConfig,
 	sinkURI *url.URL,
-) (*sink, error) {
+) (*Sink, error) {
 	cfg, db, err := mysql.NewMysqlConfigAndDB(ctx, changefeedID, sinkURI, config)
 	if err != nil {
 		return nil, err
@@ -91,9 +91,9 @@ func newMySQLSink(
 	changefeedID common.ChangeFeedID,
 	cfg *mysql.Config,
 	db *sql.DB,
-) *sink {
+) *Sink {
 	stat := metrics.NewStatistics(changefeedID, "TxnSink")
-	result := &sink{
+	result := &Sink{
 		changefeedID: changefeedID,
 		db:           db,
 		dmlWriter:    make([]*mysql.Writer, cfg.WorkerCount),
@@ -114,7 +114,7 @@ func newMySQLSink(
 	return result
 }
 
-func (s *sink) Run(ctx context.Context) error {
+func (s *Sink) Run(ctx context.Context) error {
 	g, ctx := errgroup.WithContext(ctx)
 	g.Go(func() error {
 		return s.conflictDetector.Run(ctx)
@@ -129,7 +129,7 @@ func (s *sink) Run(ctx context.Context) error {
 	return err
 }
 
-func (s *sink) runDMLWriter(ctx context.Context, idx int) error {
+func (s *Sink) runDMLWriter(ctx context.Context, idx int) error {
 	namespace := s.changefeedID.Namespace()
 	changefeed := s.changefeedID.Name()
 
@@ -200,23 +200,23 @@ func (s *sink) runDMLWriter(ctx context.Context, idx int) error {
 	}
 }
 
-func (s *sink) IsNormal() bool {
+func (s *Sink) IsNormal() bool {
 	return s.isNormal.Load()
 }
 
-func (s *sink) SinkType() common.SinkType {
+func (s *Sink) SinkType() common.SinkType {
 	return common.MysqlSinkType
 }
 
-func (s *sink) SetTableSchemaStore(tableSchemaStore *util.TableSchemaStore) {
+func (s *Sink) SetTableSchemaStore(tableSchemaStore *util.TableSchemaStore) {
 	s.ddlWriter.SetTableSchemaStore(tableSchemaStore)
 }
 
-func (s *sink) AddDMLEvent(event *commonEvent.DMLEvent) {
+func (s *Sink) AddDMLEvent(event *commonEvent.DMLEvent) {
 	s.conflictDetector.Add(event)
 }
 
-func (s *sink) WriteBlockEvent(event commonEvent.BlockEvent) error {
+func (s *Sink) WriteBlockEvent(event commonEvent.BlockEvent) error {
 	var err error
 	switch event.GetType() {
 	case commonEvent.TypeDDLEvent:
@@ -237,9 +237,15 @@ func (s *sink) WriteBlockEvent(event commonEvent.BlockEvent) error {
 	return nil
 }
 
-func (s *sink) AddCheckpointTs(_ uint64) {}
+func (s *Sink) AddCheckpointTs(_ uint64) {}
 
-func (s *sink) GetStartTsList(
+// GetStartTsList return the startTs list and startTsIsSyncpoint list for each table in the tableIDs list.
+// If removeDDLTs is true, we just need to remove the ddl ts item for this changefeed, and return startTsList directly.
+// If removeDDLTs is false, we need to query the ddl ts from the ddl_ts table, and return the startTs list and startTsIsSyncpoint list.
+// The startTsIsSyncpoint list is used to determine whether the startTs is a syncpoint event.
+// when the startTs in input list is larger than the the startTs from ddlTs,
+// we need to set the related startTsIsSyncpoint to false, and return the input startTs value.
+func (s *Sink) GetStartTsList(
 	tableIds []int64,
 	startTsList []int64,
 	removeDDLTs bool,
@@ -269,7 +275,7 @@ func (s *sink) GetStartTsList(
 	return startTsList, isSyncpointList, nil
 }
 
-func (s *sink) Close(removeChangefeed bool) {
+func (s *Sink) Close(removeChangefeed bool) {
 	// when remove the changefeed, we need to remove the ddl ts item in the ddl worker
 	if removeChangefeed {
 		if err := s.ddlWriter.RemoveDDLTsItem(); err != nil {
diff --git a/downstreamadapter/sink/mysql/sink_test.go b/downstreamadapter/sink/mysql/sink_test.go
index c416165c85..25e56231ce 100644
--- a/downstreamadapter/sink/mysql/sink_test.go
+++ b/downstreamadapter/sink/mysql/sink_test.go
@@ -28,7 +28,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func MysqlSinkForTest() (*sink, sqlmock.Sqlmock) {
+func MysqlSinkForTest() (*Sink, sqlmock.Sqlmock) {
 	db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
 	ctx := context.Background()
 	changefeedID := common.NewChangefeedID4Test("test", "test")
diff --git a/downstreamadapter/sink/pulsar/sink.go b/downstreamadapter/sink/pulsar/sink.go
index c38d27e16f..8af8f85ca5 100644
--- a/downstreamadapter/sink/pulsar/sink.go
+++ b/downstreamadapter/sink/pulsar/sink.go
@@ -192,10 +192,6 @@ func (s *sink) SetTableSchemaStore(tableSchemaStore *util.TableSchemaStore) {
 	s.tableSchemaStore = tableSchemaStore
 }
 
-func (s *sink) GetStartTsList(_ []int64, startTsList []int64, _ bool) ([]int64, []bool, error) {
-	return startTsList, make([]bool, len(startTsList)), nil
-}
-
 func (s *sink) sendCheckpoint(ctx context.Context) error {
 	checkpointTsMessageDuration := metrics.CheckpointTsMessageDuration.WithLabelValues(s.changefeedID.Namespace(), s.changefeedID.Name())
 	checkpointTsMessageCount := metrics.CheckpointTsMessageCount.WithLabelValues(s.changefeedID.Namespace(), s.changefeedID.Name())
diff --git a/downstreamadapter/sink/sink.go b/downstreamadapter/sink/sink.go
index fd5b1df7af..d09a217955 100644
--- a/downstreamadapter/sink/sink.go
+++ b/downstreamadapter/sink/sink.go
@@ -38,7 +38,6 @@ type Sink interface {
 	WriteBlockEvent(event commonEvent.BlockEvent) error
 	AddCheckpointTs(ts uint64)
 
-	GetStartTsList(tableIds []int64, startTsList []int64, removeDDLTs bool) ([]int64, []bool, error)
 	SetTableSchemaStore(tableSchemaStore *util.TableSchemaStore)
 	Close(removeChangefeed bool)
 	Run(ctx context.Context) error
diff --git a/pkg/sink/mysql/mysql_writer_for_ddl_ts.go b/pkg/sink/mysql/mysql_writer_for_ddl_ts.go
index 6591f75ae8..5949d14a66 100644
--- a/pkg/sink/mysql/mysql_writer_for_ddl_ts.go
+++ b/pkg/sink/mysql/mysql_writer_for_ddl_ts.go
@@ -269,7 +269,7 @@ func dropItemQuery(dropTableIds []int64, ticdcClusterID string, changefeedID str
 	return builder.String()
 }
 
-// GetStartTsList return the startTs list for each table in the tableIDs list.
+// GetStartTsList return the startTs list and startTsIsSyncpoint list for each table in the tableIDs list.
 // For each table,
 //  1. If no ddl-ts-v1 table or no the row for the table , startTs = 0; -- means the table is new.
 //  2. Else,
@@ -280,6 +280,9 @@ func dropItemQuery(dropTableIds []int64, ticdcClusterID string, changefeedID str
 //     (we use related table to find the ddl job -- take `truncate table` as an example, the ddl job used the table truncated, but not the new table)
 //     2.2.2.1 if the latest ddl job time is larger than the createdAt, startTs = ddlTs
 //     2.2.2.2 else startTs = ddlTs - 1
+//
+// for the startTsIsSyncpoint List, only when the ddlTs is finished or we query find the last ddl job time is larger than the createdAt,
+// the startTsIsSyncpoint equals the query result in ddl_ts table, otherwise, the startTsIsSyncpoint is false.
 func (w *Writer) GetStartTsList(tableIDs []int64) ([]int64, []bool, error) {
 	retStartTsList := make([]int64, len(tableIDs))
 	tableIdIdxMap := make(map[int64]int, len(tableIDs))


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @hongyunyan, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
Hello team, gemini-code-assist here to provide a summary of this pull request authored by @hongyunyan.

The primary goal of this PR is to refine the `Sink` interface by removing the `GetStartTsList` method. This method's functionality, which involves querying historical DDL information to determine the correct starting timestamp for tables, is specific to the MySQL sink implementation. Therefore, it's being moved out of the generic `Sink` interface and made a concrete method of the `mysql.Sink` type. The `EventDispatcherManager` is updated to reflect this change, conditionally calling `GetStartTsList` only when dealing with a MySQL sink.

### Highlights
* **Interface Refinement**: The `GetStartTsList` method has been removed from the generic `downstreamadapter/sink/sink.go` interface, making the interface more focused on common sink operations.
* **MySQL Specific Functionality**: The `GetStartTsList` method is now exclusively part of the concrete `mysql.Sink` type, acknowledging its specific implementation details related to querying DDL history from the downstream MySQL database.
* **Dispatcher Logic Update**: The `EventDispatcherManager`'s `newDispatchers` function has been updated to check the sink type. It now only attempts to call `GetStartTsList` if the sink is of type `MysqlSinkType`, using a type assertion to access the concrete `mysql.Sink` methods.
* **Code Cleanup**: Unused implementations of `GetStartTsList` have been removed from other sink types like blackhole, cloudstorage, and kafka, simplifying their code.
* **Struct Renaming**: The `sink` struct in `downstreamadapter/sink/mysql/sink.go` has been renamed to `Sink` (capitalized), making it publicly accessible.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **downstreamadapter/dispatcher/dispatcher_test.go**
  * Removed the `GetStartTsList` method from the `mockSink` struct.
* **downstreamadapter/dispatchermanager/event_dispatcher_manager.go**
  * Added import for the `mysql` sink package.
  * Modified the `newDispatchers` function to conditionally call `GetStartTsList` only for MySQL sinks using a type assertion (`e.sink.(*mysql.Sink)`).
* **downstreamadapter/sink/blackhole/sink.go**
  * Removed the `GetStartTsList` method.
* **downstreamadapter/sink/cloudstorage/sink.go**
  * Removed the `GetStartTsList` method.
* **downstreamadapter/sink/kafka/sink.go**
  * Removed the `GetStartTsList` method.
* **downstreamadapter/sink/mysql/sink.go**
  * Renamed the `sink` struct to `Sink` (lines 44, 81, 94, 96).
  * Updated method receivers from `*sink` to `*Sink` (lines 117, 132, 203, 207, 211, 215, 219, 240, 248, 278).
  * Added detailed comments to the `GetStartTsList` method explaining its specific logic for MySQL, including handling `removeDDLTs` and `startTsIsSyncpointList` based on querying the `ddl_ts` table (lines 242-247).
* **downstreamadapter/sink/sink.go**
  * Removed the `GetStartTsList` method from the `Sink` interface (line 41).
* **pkg/sink/mysql/mysql_writer_for_ddl_ts.go**
  * Updated comments for the `GetStartTsList` function to clarify its return values and logic regarding syncpoints and the `ddl_ts` table (lines 272, 283-285).

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> An interface so clean,
> No method left unseen,
> MySQL takes its part,
> A specialized art,
> Keeping the code lean.


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T08:42:47Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request effectively refactors the `GetStartTsList` method by removing it from the generic `Sink` interface and making it a specialized function of the `mysql.Sink`. This is a good change that improves interface segregation and clarity, as this functionality is specific to the MySQL sink's needs.

The changes are well-contained and logical. The renaming of the internal `sink` struct to `Sink` in the `mysql` package is a necessary consequence to allow type assertion from other packages and has been handled correctly.

I have one minor suggestion for code cleanup in `event_dispatcher_manager.go`.

Please also ensure to fill out the PR description and release note sections, as they are currently using the default templates.

### Summary of Findings
* **Code Refinement**: Identified a redundant error check in `event_dispatcher_manager.go` that can be removed to improve code clarity.
* **PR Housekeeping**: The pull request description and release note sections are currently using template content and should be updated with relevant information.

### Merge Readiness
The pull request is well-structured and achieves its goal of refactoring `GetStartTsList`. There is one minor suggestion for code cleanup. Once that is addressed and the PR description/release notes are updated, this PR should be in good shape for merging. As an AI, I am not authorized to approve pull requests; please ensure other reviewers approve this change before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-05-21T08:44:15Z
user: login: 3AceShowHand
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T10:14:16Z
user: login: 3AceShowHand
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-21T10:14:21Z

Review comments:
diff_hunk: @@ -475,7 +476,30 @@ func (e *EventDispatcherManager) newDispatchers(infos []dispatcherCreateInfo, re
 	// so we just return the startTs we get.
 	// Besides, we batch the creation for the dispatchers,
 	// mainly because we need to batch the query for startTs when sink is mysql-class to reduce the time cost.
-	newStartTsList, startTsIsSyncpointList, err := e.sink.GetStartTsList(tableIds, startTsList, removeDDLTs)
+	//
+	// When we enable syncpoint, we also need to know the last ddl commitTs whether is a syncpoint event.
+	// because the commitTs of a syncpoint event can be the same as a ddl event
+	// If there is a ddl event and a syncpoint event at the same time, we ensure the syncpoint event always after the ddl event.
+	// So we need to know whether the commitTs is from a syncpoint event or a ddl event,
+	// to decide whether we need to send generate the syncpoint event of this commitTs to downstream.
+	var newStartTsList []int64
+	startTsIsSyncpointList := make([]bool, len(startTsList))
+	var err error
+	if e.sink.SinkType() == common.MysqlSinkType {
+		newStartTsList, startTsIsSyncpointList, err = e.sink.(*mysql.Sink).GetStartTsList(tableIds, startTsList, removeDDLTs)
+		if err != nil {
+			return errors.Trace(err)
+		}
+		log.Info("calculate real startTs for dispatchers",
+			zap.Stringer("changefeedID", e.changefeedID),
+			zap.Any("receiveStartTs", startTsList),
+			zap.Any("realStartTs", newStartTsList),
+			zap.Bool("removeDDLTs", removeDDLTs),
+		)
+	} else {
+		newStartTsList = startTsList
+	}
+
 	if err != nil {
 		return errors.Trace(err)
 	}
path: downstreamadapter/dispatchermanager/event_dispatcher_manager.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

This error check for `err != nil` seems to be redundant. 

Here's why:
1. `err` is declared via `var err error` before the `if/else` block that handles different sink types.
2. If `e.sink.SinkType() == common.MysqlSinkType` is true:
   - `err` is assigned the result of `e.sink.(*mysql.Sink).GetStartTsList(...)`.
   - If this call results in an error, the function returns early at line 491 (`if err != nil { return errors.Trace(err) }`).
   - Therefore, if execution reaches this point (line 503) after the MySQL sink path, `err` must be `nil`.
3. If the `else` block (for non-MySQL sinks) is taken:
   - `err` retains its initial `nil` value as it's not modified in this path.

In both scenarios, `err` will be `nil` when this check is reached, making the condition `err != nil` always false. Could this block be removed for clarity?
created_at: 2025-05-21T08:44:15Z
author_association: NONE
diff_hunk: @@ -475,10 +476,30 @@ func (e *EventDispatcherManager) newDispatchers(infos []dispatcherCreateInfo, re
 	// so we just return the startTs we get.
 	// Besides, we batch the creation for the dispatchers,
 	// mainly because we need to batch the query for startTs when sink is mysql-class to reduce the time cost.
-	newStartTsList, startTsIsSyncpointList, err := e.sink.GetStartTsList(tableIds, startTsList, removeDDLTs)
-	if err != nil {
-		return errors.Trace(err)
+	//
+	// When we enable syncpoint, we also need to know the last ddl commitTs whether is a syncpoint event.
+	// because the commitTs of a syncpoint event can be the same as a ddl event
+	// If there is a ddl event and a syncpoint event at the same time, we ensure the syncpoint event always after the ddl event.
+	// So we need to know whether the commitTs is from a syncpoint event or a ddl event,
+	// to decide whether we need to send generate the syncpoint event of this commitTs to downstream.
+	var newStartTsList []int64
+	startTsIsSyncpointList := make([]bool, len(startTsList))
path: downstreamadapter/dispatchermanager/event_dispatcher_manager.go
user: login: 3AceShowHand
body: you can merge these 2 var declarations together
created_at: 2025-05-21T10:14:16Z
author_association: COLLABORATOR

PR comments:
user: login: ti-chi-bot[bot]
created_at: 2025-05-21T08:44:17Z
body: @gemini-code-assist[bot]: adding LGTM is restricted to approvers and reviewers in OWNERS files.

<details>

In response to [this](https://github.com/pingcap/ticdc/pull/1350#pullrequestreview-2856850834):

>## Code Review
>This pull request effectively refactors the `GetStartTsList` method by removing it from the generic `Sink` interface and making it a specialized function of the `mysql.Sink`. This is a good change that improves interface segregation and clarity, as this functionality is specific to the MySQL sink's needs.
>
>The changes are well-contained and logical. The renaming of the internal `sink` struct to `Sink` in the `mysql` package is a necessary consequence to allow type assertion from other packages and has been handled correctly.
>
>I have one minor suggestion for code cleanup in `event_dispatcher_manager.go`.
>
>Please also ensure to fill out the PR description and release note sections, as they are currently using the default templates.
>
>### Summary of Findings
>* **Code Refinement**: Identified a redundant error check in `event_dispatcher_manager.go` that can be removed to improve code clarity.
>* **PR Housekeeping**: The pull request description and release note sections are currently using template content and should be updated with relevant information.
>
>### Merge Readiness
>The pull request is well-structured and achieves its goal of refactoring `GetStartTsList`. There is one minor suggestion for code cleanup. Once that is addressed and the PR description/release notes are updated, this PR should be in good shape for merging. As an AI, I am not authorized to approve pull requests; please ensure other reviewers approve this change before merging.


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-05-21T09:18:01Z
body: 
[FORMAT CHECKER NOTIFICATION]

**Notice**: To remove the `do-not-merge/needs-linked-issue` label, please provide the linked issue number on one line in the PR body, for example: `Issue Number: close #123` or `Issue Number: ref #456`.

<sub>:open_book: For more info, you can check the ["Contribute Code"](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/contribute-code.html#referring-to-an-issue) section in the development guide.</sub>


<!--Checker Notification Identifier-->

author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-05-21T09:42:19Z
body: @hongyunyan: 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-light | 82a85d34f5f8ec7a86b1a73f0aae47b6a03ef029 | [link](https://do.pingcap.net/jenkins/job/pingcap/job/ticdc/job/pull_cdc_pulsar_integration_light/598/display/redirect) | false | `/test pull-cdc-pulsar-integration-light`

[Full PR test history](https://prow.tidb.net/pr-history?org=pingcap&repo=ticdc&pr=1350). [Your PR dashboard](https://prow.tidb.net/pr?query=is%3Apr%20state%3Aopen%20author%3Ahongyunyan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-05-21T10:14:29Z
body: [LGTM Timeline notifier]
---
Timeline:

- `2025-05-21 10:14:29.303578729 +0000 UTC m=+973.720638292`: :ballot_box_with_check: agreed by [3AceShowHand](https://github.com/3AceShowHand).
author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-05-21T10:14:29Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/pingcap/ticdc/pull/1350#pullrequestreview-2857131498" title="Approved">3AceShowHand</a>*

The full list of commands accepted by this bot can be found [here](https://prow.tidb.net/command-help?repo=pingcap%2Fticdc).

The pull request process is described [here](https://book.prow.tidb.net/#/workflows/pr)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/pingcap/ticdc/blob/master/OWNERS)~~ [3AceShowHand]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE