This is an automated email from the ASF dual-hosted git repository.

ruanhang1993 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git


The following commit(s) were added to refs/heads/master by this push:
     new 2ae8f874f [FLINK-40741][cdc-connector][mysql] Fix newly added table 
recovery after region failover (#4557)
2ae8f874f is described below

commit 2ae8f874f2ccb57b6ee882c0b75655f1f795b479
Author: Hang Ruan <[email protected]>
AuthorDate: Wed Sep 23 11:51:05 2026 +0800

    [FLINK-40741][cdc-connector][mysql] Fix newly added table recovery after 
region failover (#4557)
---
 .../source/enumerator/MySqlSourceEnumerator.java   |  25 +++-
 .../mysql/source/reader/MySqlSourceReader.java     |   6 +
 .../enumerator/MySqlSourceEnumeratorTest.java      | 136 +++++++++++++++++++++
 .../mysql/source/reader/MySqlSourceReaderTest.java |  28 +++++
 4 files changed, 191 insertions(+), 4 deletions(-)

diff --git 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumerator.java
 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumerator.java
index ff0279e4a..8696877f8 100644
--- 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumerator.java
+++ 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumerator.java
@@ -60,6 +60,7 @@ import java.util.Optional;
 import java.util.TreeSet;
 import java.util.stream.Collectors;
 
+import static 
org.apache.flink.cdc.connectors.mysql.source.assigners.AssignerStatus.isNewlyAddedAssigningFinished;
 import static 
org.apache.flink.cdc.connectors.mysql.source.assigners.AssignerStatus.isNewlyAddedAssigningSnapshotFinished;
 
 /**
@@ -146,9 +147,17 @@ public class MySqlSourceEnumerator implements 
SplitEnumerator<MySqlSplit, Pendin
 
     @Override
     public void addReader(int subtaskId) {
-        // send BinlogSplitUpdateRequestEvent to source reader after newly 
added table
-        // snapshot splits finished.
-        if 
(isNewlyAddedAssigningSnapshotFinished(splitAssigner.getAssignerStatus())) {
+        // Reconcile the binlog split after newly added table snapshot splits 
finished. The
+        // NEWLY_ADDED_ASSIGNING_FINISHED branch is needed for region 
failover: the enumerator
+        // remains alive with the latest finished split metadata, while the 
restarted reader may
+        // restore an older binlog split from the last completed checkpoint.
+        if 
(isNewlyAddedAssigningSnapshotFinished(splitAssigner.getAssignerStatus())
+                || 
isNewlyAddedAssigningFinished(splitAssigner.getAssignerStatus())) {
+            LOG.info(
+                    "The enumerator sends BinlogSplitUpdateRequestEvent to 
subtask {} when adding "
+                            + "reader under assigner status {}.",
+                    subtaskId,
+                    splitAssigner.getAssignerStatus());
             context.sendEventToSourceReader(subtaskId, new 
BinlogSplitUpdateRequestEvent());
         }
     }
@@ -181,7 +190,15 @@ public class MySqlSourceEnumerator implements 
SplitEnumerator<MySqlSplit, Pendin
             LOG.info(
                     "The enumerator receives event that the binlog split has 
been updated from subtask {}. ",
                     subtaskId);
-            splitAssigner.onBinlogSplitUpdated();
+            if 
(isNewlyAddedAssigningFinished(splitAssigner.getAssignerStatus())) {
+                LOG.info(
+                        "Ignore the duplicated binlog split update 
acknowledgement from subtask {} "
+                                + "under assigner status {}.",
+                        subtaskId,
+                        splitAssigner.getAssignerStatus());
+            } else {
+                splitAssigner.onBinlogSplitUpdated();
+            }
         } else if (sourceEvent instanceof 
LatestFinishedSplitsNumberRequestEvent) {
             LOG.info(
                     "The enumerator receives request from subtask {} for the 
latest finished splits number after added newly tables. ",
diff --git 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReader.java
 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReader.java
index 4d6f4a862..48207aace 100644
--- 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReader.java
+++ 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReader.java
@@ -287,6 +287,12 @@ public class MySqlSourceReader<T>
                 // the binlog split is suspended
                 if (binlogSplit.isSuspended()) {
                     suspendedBinlogSplit = binlogSplit;
+                    // A suspended split can be restored directly from a 
checkpoint after region
+                    // failover. In that case there is no running binlog 
reader to finish and send
+                    // this request from onSplitFinished(), so request the 
latest finished split
+                    // count here to resume the update protocol.
+                    context.sendSourceEventToCoordinator(
+                            new LatestFinishedSplitsNumberRequestEvent());
                 } else if (!binlogSplit.isCompletedSplit()) {
                     uncompletedBinlogSplits.put(binlogSplit.splitId(), 
binlogSplit);
                     requestBinlogSplitMetaIfNeeded(binlogSplit);
diff --git 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumeratorTest.java
 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumeratorTest.java
index 7fb220664..232497896 100644
--- 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumeratorTest.java
+++ 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/enumerator/MySqlSourceEnumeratorTest.java
@@ -18,16 +18,31 @@
 package org.apache.flink.cdc.connectors.mysql.source.enumerator;
 
 import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.api.connector.source.SourceEvent;
+import org.apache.flink.cdc.connectors.mysql.source.assigners.AssignerStatus;
 import 
org.apache.flink.cdc.connectors.mysql.source.assigners.MySqlBinlogSplitAssigner;
+import 
org.apache.flink.cdc.connectors.mysql.source.assigners.MySqlSplitAssigner;
+import 
org.apache.flink.cdc.connectors.mysql.source.assigners.state.PendingSplitsState;
 import org.apache.flink.cdc.connectors.mysql.source.config.MySqlSourceConfig;
 import 
org.apache.flink.cdc.connectors.mysql.source.config.MySqlSourceConfigFactory;
 import 
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitMetaAssembledEvent;
+import 
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitUpdateAckEvent;
+import 
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitUpdateRequestEvent;
+import org.apache.flink.cdc.connectors.mysql.source.offset.BinlogOffset;
+import 
org.apache.flink.cdc.connectors.mysql.source.split.FinishedSnapshotSplitInfo;
+import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSplit;
+import 
org.apache.flink.cdc.connectors.mysql.source.utils.MockMySqlSplitEnumeratorEnumeratorContext;
 import org.apache.flink.cdc.connectors.mysql.table.StartupOptions;
 
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.time.ZoneId;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
 import static 
org.apache.flink.cdc.connectors.mysql.testutils.MetricsUtils.getMySqlSplitEnumeratorContext;
 
@@ -63,4 +78,125 @@ class MySqlSourceEnumeratorTest {
                                         0, new 
BinlogSplitMetaAssembledEvent(0L)))
                 .doesNotThrowAnyException();
     }
+
+    @Test
+    void testAddReaderRequestsBinlogSplitUpdateAfterRegionFailover() throws 
Exception {
+        MockMySqlSplitEnumeratorEnumeratorContext context =
+                new MockMySqlSplitEnumeratorEnumeratorContext(1);
+        TestingSplitAssigner splitAssigner =
+                new 
TestingSplitAssigner(AssignerStatus.NEWLY_ADDED_ASSIGNING_FINISHED);
+        MySqlSourceEnumerator enumerator =
+                new MySqlSourceEnumerator(
+                        context, null, splitAssigner, 
Boundedness.CONTINUOUS_UNBOUNDED);
+
+        enumerator.addReader(0);
+
+        List<SourceEvent> sentEvents = context.getSentSourceEvent().get(0);
+        Assertions.assertThat(sentEvents)
+                .singleElement()
+                .isInstanceOf(BinlogSplitUpdateRequestEvent.class);
+    }
+
+    @Test
+    void testBinlogSplitUpdateAckIsIdempotentAfterRegionFailover() {
+        MockMySqlSplitEnumeratorEnumeratorContext context =
+                new MockMySqlSplitEnumeratorEnumeratorContext(1);
+        TestingSplitAssigner splitAssigner =
+                new 
TestingSplitAssigner(AssignerStatus.NEWLY_ADDED_ASSIGNING_FINISHED);
+        MySqlSourceEnumerator enumerator =
+                new MySqlSourceEnumerator(
+                        context, null, splitAssigner, 
Boundedness.CONTINUOUS_UNBOUNDED);
+
+        enumerator.handleSourceEvent(0, new BinlogSplitUpdateAckEvent());
+
+        
Assertions.assertThat(splitAssigner.getBinlogSplitUpdatedCount()).isZero();
+        Assertions.assertThat(splitAssigner.getAssignerStatus())
+                .isEqualTo(AssignerStatus.NEWLY_ADDED_ASSIGNING_FINISHED);
+    }
+
+    @Test
+    void testBinlogSplitUpdateAckCompletesNormalUpdate() {
+        MockMySqlSplitEnumeratorEnumeratorContext context =
+                new MockMySqlSplitEnumeratorEnumeratorContext(1);
+        TestingSplitAssigner splitAssigner =
+                new 
TestingSplitAssigner(AssignerStatus.NEWLY_ADDED_ASSIGNING_SNAPSHOT_FINISHED);
+        MySqlSourceEnumerator enumerator =
+                new MySqlSourceEnumerator(
+                        context, null, splitAssigner, 
Boundedness.CONTINUOUS_UNBOUNDED);
+
+        enumerator.handleSourceEvent(0, new BinlogSplitUpdateAckEvent());
+
+        
Assertions.assertThat(splitAssigner.getBinlogSplitUpdatedCount()).isOne();
+        Assertions.assertThat(splitAssigner.getAssignerStatus())
+                .isEqualTo(AssignerStatus.NEWLY_ADDED_ASSIGNING_FINISHED);
+    }
+
+    private static class TestingSplitAssigner implements MySqlSplitAssigner {
+        private AssignerStatus assignerStatus;
+        private int binlogSplitUpdatedCount;
+
+        private TestingSplitAssigner(AssignerStatus assignerStatus) {
+            this.assignerStatus = assignerStatus;
+        }
+
+        @Override
+        public void open() {}
+
+        @Override
+        public Optional<MySqlSplit> getNext() {
+            return Optional.empty();
+        }
+
+        @Override
+        public boolean waitingForFinishedSplits() {
+            return false;
+        }
+
+        @Override
+        public List<FinishedSnapshotSplitInfo> getFinishedSplitInfos() {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public void onFinishedSplits(Map<String, BinlogOffset> 
splitFinishedOffsets) {}
+
+        @Override
+        public void addSplits(Collection<MySqlSplit> splits) {}
+
+        @Override
+        public PendingSplitsState snapshotState(long checkpointId) {
+            return null;
+        }
+
+        @Override
+        public void notifyCheckpointComplete(long checkpointId) {}
+
+        @Override
+        public AssignerStatus getAssignerStatus() {
+            return assignerStatus;
+        }
+
+        @Override
+        public void startAssignNewlyAddedTables() {
+            assignerStatus = assignerStatus.startAssignNewlyTables();
+        }
+
+        @Override
+        public boolean noMoreSplits() {
+            return true;
+        }
+
+        @Override
+        public void onBinlogSplitUpdated() {
+            binlogSplitUpdatedCount++;
+            assignerStatus = assignerStatus.onBinlogSplitUpdated();
+        }
+
+        @Override
+        public void close() {}
+
+        private int getBinlogSplitUpdatedCount() {
+            return binlogSplitUpdatedCount;
+        }
+    }
 }
diff --git 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReaderTest.java
 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReaderTest.java
index 137df37c5..93d8a42f0 100644
--- 
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReaderTest.java
+++ 
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/reader/MySqlSourceReaderTest.java
@@ -31,6 +31,7 @@ import 
org.apache.flink.cdc.connectors.mysql.source.config.MySqlSourceConfigFact
 import 
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitMetaAssembledEvent;
 import 
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitMetaEvent;
 import 
org.apache.flink.cdc.connectors.mysql.source.events.FinishedSnapshotSplitsAckEvent;
+import 
org.apache.flink.cdc.connectors.mysql.source.events.LatestFinishedSplitsNumberRequestEvent;
 import 
org.apache.flink.cdc.connectors.mysql.source.metrics.MySqlSourceReaderMetrics;
 import org.apache.flink.cdc.connectors.mysql.source.offset.BinlogOffset;
 import 
org.apache.flink.cdc.connectors.mysql.source.split.FinishedSnapshotSplitInfo;
@@ -122,6 +123,33 @@ class MySqlSourceReaderTest extends MySqlSourceTestBase {
         inventoryDatabase.dropDatabase();
     }
 
+    @Test
+    void testRestoredSuspendedBinlogSplitRequestsLatestFinishedSplitNumber() 
throws Exception {
+        MySqlSourceConfig sourceConfig = getConfig(new String[] {"customers"});
+        TestingReaderContext readerContext = new TestingReaderContext();
+        MySqlSourceReader<SourceRecord> reader =
+                createReader(sourceConfig, readerContext, 0, 
SnapshotPhaseHooks.empty());
+        MySqlBinlogSplit binlogSplit =
+                new MySqlBinlogSplit(
+                        MySqlBinlogSplitAssigner.BINLOG_SPLIT_ID,
+                        BinlogOffset.ofBinlogFilePosition("mysql-bin.000001", 
4L),
+                        BinlogOffset.ofNonStopping(),
+                        Collections.emptyList(),
+                        Collections.emptyMap(),
+                        0);
+
+        reader.addSplits(
+                
Collections.singletonList(MySqlBinlogSplit.toSuspendedBinlogSplit(binlogSplit)));
+
+        Assertions.assertThat(readerContext.getSentEvents())
+                .anySatisfy(
+                        event ->
+                                Assertions.assertThat(event)
+                                        .isInstanceOf(
+                                                
LatestFinishedSplitsNumberRequestEvent.class));
+        reader.close();
+    }
+
     @Test
     void testRemoveTableUsingStateFromSnapshotPhase() throws Exception {
         customerDatabase.createAndInitialize();

Reply via email to