This is an automated email from the ASF dual-hosted git repository.
leonardBang 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 d9766a35b [FLINK-40216][cdc-base] Preserve assignment order of splits
across restore to keep meta groups consistent (#4489)
d9766a35b is described below
commit d9766a35bc286ffb0238008e827a7d43c5debc41
Author: κΉλν <[email protected]>
AuthorDate: Fri Oct 23 10:56:19 2026 +0900
[FLINK-40216][cdc-base] Preserve assignment order of splits across restore
to keep meta groups consistent (#4489)
This closes #4489.
---
.../base/source/assigner/HybridSplitAssigner.java | 6 +-
.../source/assigner/SnapshotSplitAssigner.java | 29 ++--
.../state/PendingSplitsStateSerializer.java | 17 +-
.../base/source/meta/split/StreamSplit.java | 39 ++++-
.../source/reader/IncrementalSourceReader.java | 47 ++----
.../source/assigner/MetaGroupOrderingTest.java | 182 +++++++++++++++++++++
.../state/PendingSplitsStateSerializerTest.java | 51 ++++++
.../state/SnapshotPendingSplitsStateTest.java | 114 +++++++++++++
.../base/source/meta/split/StreamSplitTest.java | 62 +++++++
.../source/reader/IncrementalSourceReaderTest.java | 60 +++++++
10 files changed, 543 insertions(+), 64 deletions(-)
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/HybridSplitAssigner.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/HybridSplitAssigner.java
index 046933e16..6c8202491 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/HybridSplitAssigner.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/HybridSplitAssigner.java
@@ -38,12 +38,10 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.stream.Collectors;
import static
org.apache.flink.cdc.connectors.base.source.assigner.AssignerStatus.isInitialAssigningFinished;
import static
org.apache.flink.cdc.connectors.base.source.assigner.AssignerStatus.isNewlyAddedAssigningFinished;
@@ -303,9 +301,7 @@ public class HybridSplitAssigner<C extends SourceConfig>
implements SplitAssigne
"Cannot create a stream split after the snapshot split
metadata was released.");
}
final List<SchemalessSnapshotSplit> assignedSnapshotSplit =
- snapshotSplitAssigner.getAssignedSplits().values().stream()
- .sorted(Comparator.comparing(SourceSplitBase::splitId))
- .collect(Collectors.toList());
+ new
ArrayList<>(snapshotSplitAssigner.getAssignedSplits().values());
Map<String, Offset> splitFinishedOffsets =
snapshotSplitAssigner.getSplitFinishedOffsets();
final List<FinishedSnapshotSplitInfo> finishedSnapshotSplitInfos = new
ArrayList<>();
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/SnapshotSplitAssigner.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/SnapshotSplitAssigner.java
index 633f08957..5eda2be6c 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/SnapshotSplitAssigner.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/SnapshotSplitAssigner.java
@@ -29,6 +29,7 @@ import
org.apache.flink.cdc.connectors.base.source.meta.split.SchemalessSnapshot
import org.apache.flink.cdc.connectors.base.source.meta.split.SnapshotSplit;
import org.apache.flink.cdc.connectors.base.source.meta.split.SourceSplitBase;
import
org.apache.flink.cdc.connectors.base.source.metrics.SourceEnumeratorMetrics;
+import
org.apache.flink.cdc.connectors.base.source.reader.IncrementalSourceReader;
import org.apache.flink.util.FlinkRuntimeException;
import org.apache.flink.util.Preconditions;
@@ -72,7 +73,21 @@ public class SnapshotSplitAssigner<C extends SourceConfig>
implements SplitAssig
private final List<TableId> alreadyProcessedTables;
private final List<SchemalessSnapshotSplit> remainingSplits;
- private final Map<String, SchemalessSnapshotSplit> assignedSplits;
+
+ /**
+ * The splits that have been assigned to a reader. Once a split is
finished, it remains in this
+ * map. An entry added to {@link #splitFinishedOffsets} indicates that the
split has been
+ * finished. If reading the split fails, it is removed from this map.
+ *
+ * <p>{@link IncrementalSourceReader} relies on the order of elements
within the map:
+ *
+ * <ol>
+ * <li>It must correspond to the order of assignment of the splits to
readers.
+ * <li>The order must be retained across job restarts.
+ * </ol>
+ */
+ private final LinkedHashMap<String, SchemalessSnapshotSplit>
assignedSplits;
+
private final Map<TableId, TableChanges.TableChange> tableSchemas;
private final Map<String, Offset> splitFinishedOffsets;
@@ -171,17 +186,7 @@ public class SnapshotSplitAssigner<C extends SourceConfig>
implements SplitAssig
this.currentParallelism = currentParallelism;
this.alreadyProcessedTables = alreadyProcessedTables;
this.remainingSplits = remainingSplits;
- // When job restore from savepoint, sort the existing tables and newly
added tables
- // to let enumerator only send newly added tables' StreamSplitMetaEvent
- this.assignedSplits =
- assignedSplits.entrySet().stream()
- .sorted(Map.Entry.comparingByKey())
- .collect(
- Collectors.toMap(
- Map.Entry::getKey,
- Map.Entry::getValue,
- (o, o2) -> o,
- LinkedHashMap::new));
+ this.assignedSplits = new LinkedHashMap<>(assignedSplits);
this.tableSchemas = tableSchemas;
this.splitFinishedOffsets = splitFinishedOffsets;
this.assignerStatus = assignerStatus;
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializer.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializer.java
index d07c2320a..e1430eff4 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializer.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializer.java
@@ -35,6 +35,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -236,12 +237,12 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
int splitVersion, DataInputDeserializer in) throws IOException {
List<TableId> alreadyProcessedTables = readTableIds(2, in);
List<SnapshotSplit> remainingSplits = readSnapshotSplits(splitVersion,
in);
- Map<String, SnapshotSplit> assignedSnapshotSplits =
+ LinkedHashMap<String, SnapshotSplit> assignedSnapshotSplits =
readAssignedSnapshotSplits(splitVersion, in);
final List<SchemalessSnapshotSplit> remainingSchemalessSplits = new
ArrayList<>();
- final Map<String, SchemalessSnapshotSplit>
assignedSchemalessSnapshotSplits =
- new HashMap<>();
+ final LinkedHashMap<String, SchemalessSnapshotSplit>
assignedSchemalessSnapshotSplits =
+ new LinkedHashMap<>();
final Map<TableId, TableChanges.TableChange> tableSchemas = new
HashMap<>();
remainingSplits.forEach(
split -> {
@@ -290,7 +291,7 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
int version, int splitVersion, DataInputDeserializer in) throws
IOException {
List<TableId> alreadyProcessedTables = readTableIds(version, in);
List<SnapshotSplit> remainingSplits = readSnapshotSplits(splitVersion,
in);
- Map<String, SnapshotSplit> assignedSnapshotSplits =
+ LinkedHashMap<String, SnapshotSplit> assignedSnapshotSplits =
readAssignedSnapshotSplits(splitVersion, in);
Map<String, Offset> finishedOffsets =
readFinishedOffsets(splitVersion, in);
AssignerStatus assignerStatus;
@@ -307,8 +308,8 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
List<TableId> remainingTableIds = readTableIds(version, in);
boolean isTableIdCaseSensitive = in.readBoolean();
final List<SchemalessSnapshotSplit> remainingSchemalessSplits = new
ArrayList<>();
- final Map<String, SchemalessSnapshotSplit>
assignedSchemalessSnapshotSplits =
- new HashMap<>();
+ final LinkedHashMap<String, SchemalessSnapshotSplit>
assignedSchemalessSnapshotSplits =
+ new LinkedHashMap<>();
final Map<TableId, TableChanges.TableChange> tableSchemas = new
HashMap<>();
remainingSplits.forEach(
split -> {
@@ -444,9 +445,9 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
}
}
- private Map<String, SnapshotSplit> readAssignedSnapshotSplits(
+ private LinkedHashMap<String, SnapshotSplit> readAssignedSnapshotSplits(
int splitVersion, DataInputDeserializer in) throws IOException {
- Map<String, SnapshotSplit> assignedSplits = new HashMap<>();
+ LinkedHashMap<String, SnapshotSplit> assignedSplits = new
LinkedHashMap<>();
final int size = in.readInt();
for (int i = 0; i < size; i++) {
String splitId = in.readUTF();
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplit.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplit.java
index cdeb247f3..99b45c2ea 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplit.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplit.java
@@ -28,6 +28,7 @@ import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -42,7 +43,10 @@ public class StreamSplit extends SourceSplitBase {
private final Offset startingOffset;
private final Offset endingOffset;
+
+ /** Split IDs of all elements must be unique. */
private final List<FinishedSnapshotSplitInfo> finishedSnapshotSplitInfos;
+
private final Map<TableId, TableChange> tableSchemas;
private final int totalFinishedSplitSize;
@@ -66,6 +70,9 @@ public class StreamSplit extends SourceSplitBase {
boolean isSuspended,
boolean isSnapshotCompleted) {
super(splitId);
+
+ ensureNoDuplicates(finishedSnapshotSplitInfos);
+
this.startingOffset = startingOffset;
this.endingOffset = endingOffset;
this.finishedSnapshotSplitInfos = finishedSnapshotSplitInfos;
@@ -82,14 +89,30 @@ public class StreamSplit extends SourceSplitBase {
List<FinishedSnapshotSplitInfo> finishedSnapshotSplitInfos,
Map<TableId, TableChange> tableSchemas,
int totalFinishedSplitSize) {
- super(splitId);
- this.startingOffset = startingOffset;
- this.endingOffset = endingOffset;
- this.finishedSnapshotSplitInfos = finishedSnapshotSplitInfos;
- this.tableSchemas = tableSchemas;
- this.totalFinishedSplitSize = totalFinishedSplitSize;
- this.isSuspended = false;
- this.isSnapshotCompleted = false;
+ this(
+ splitId,
+ startingOffset,
+ endingOffset,
+ finishedSnapshotSplitInfos,
+ tableSchemas,
+ totalFinishedSplitSize,
+ false,
+ false);
+ }
+
+ private static void ensureNoDuplicates(
+ List<FinishedSnapshotSplitInfo> finishedSnapshotSplitInfos) {
+ Set<String> seenSplitIds = new HashSet<>();
+ for (FinishedSnapshotSplitInfo splitInfo : finishedSnapshotSplitInfos)
{
+ if (seenSplitIds.contains(splitInfo.getSplitId())) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Found duplicate split ID %s in finished
snapshot split infos",
+ splitInfo.getSplitId()));
+ }
+
+ seenSplitIds.add(splitInfo.getSplitId());
+ }
}
public Offset getStartingOffset() {
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReader.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReader.java
index b629d1fd6..dd68c8663 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReader.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/main/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReader.java
@@ -60,10 +60,8 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -384,28 +382,6 @@ public class IncrementalSourceReader<T, C extends
SourceConfig>
}
}
- private Set<String> getExistedSplitsOfLastGroup(
- List<FinishedSnapshotSplitInfo> finishedSnapshotSplits, int
metaGroupSize) {
- int splitsNumOfLastGroup =
- finishedSnapshotSplits.size() %
sourceConfig.getSplitMetaGroupSize();
- if (splitsNumOfLastGroup != 0) {
- int lastGroupStart =
- ((int) (finishedSnapshotSplits.size() /
sourceConfig.getSplitMetaGroupSize()))
- * metaGroupSize;
- // Keep same order with HybridSplitAssigner.createStreamSplit() to
avoid
- // 'invalid request meta group id' error
- List<String> sortedFinishedSnapshotSplits =
- finishedSnapshotSplits.stream()
- .map(FinishedSnapshotSplitInfo::getSplitId)
- .sorted()
- .collect(Collectors.toList());
- return new HashSet<>(
- sortedFinishedSnapshotSplits.subList(
- lastGroupStart, lastGroupStart +
splitsNumOfLastGroup));
- }
- return new HashSet<>();
- }
-
@Override
public void handleSourceEvents(SourceEvent sourceEvent) {
if (sourceEvent instanceof FinishedSnapshotSplitsAckEvent) {
@@ -462,15 +438,24 @@ public class IncrementalSourceReader<T, C extends
SourceConfig>
streamSplit = toNormalStreamSplit(streamSplit,
receivedTotalFinishedSplitSize);
uncompletedStreamSplits.put(streamSplit.splitId(),
streamSplit);
} else if (receivedMetaGroupId == expectedMetaGroupId) {
- Set<String> existedSplitsOfLastGroup =
- getExistedSplitsOfLastGroup(
- streamSplit.getFinishedSnapshotSplitInfos(),
- sourceConfig.getSplitMetaGroupSize());
-
+ int expectedNumberOfAlreadyRetrievedElements =
+ streamSplit.getFinishedSnapshotSplitInfos().size()
+ % sourceConfig.getSplitMetaGroupSize();
+ List<byte[]> metaGroup = metadataEvent.getMetaGroup();
+ if (expectedNumberOfAlreadyRetrievedElements > 0) {
+ LOG.info(
+ "Source reader {} is discarding the first {} out
of {} elements of meta group {}.",
+ subtaskId,
+ expectedNumberOfAlreadyRetrievedElements,
+ metaGroup.size(),
+ receivedMetaGroupId);
+ metaGroup =
+ metaGroup.subList(
+ expectedNumberOfAlreadyRetrievedElements,
metaGroup.size());
+ }
List<FinishedSnapshotSplitInfo> newAddedMetadataGroup =
- metadataEvent.getMetaGroup().stream()
+ metaGroup.stream()
.map(sourceSplitSerializer::deserialize)
- .filter(r ->
!existedSplitsOfLastGroup.contains(r.getSplitId()))
.collect(Collectors.toList());
uncompletedStreamSplits.put(
streamSplit.splitId(),
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/MetaGroupOrderingTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/MetaGroupOrderingTest.java
new file mode 100644
index 000000000..93ad35bc5
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/MetaGroupOrderingTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.connectors.base.source.assigner;
+
+import org.apache.flink.cdc.connectors.base.config.SourceConfig;
+import org.apache.flink.cdc.connectors.base.dialect.DataSourceDialect;
+import
org.apache.flink.cdc.connectors.base.source.assigner.state.ChunkSplitterState;
+import
org.apache.flink.cdc.connectors.base.source.assigner.state.SnapshotPendingSplitsState;
+import org.apache.flink.cdc.connectors.base.source.meta.offset.Offset;
+import org.apache.flink.cdc.connectors.base.source.meta.offset.OffsetFactory;
+import
org.apache.flink.cdc.connectors.base.source.meta.split.FinishedSnapshotSplitInfo;
+import
org.apache.flink.cdc.connectors.base.source.meta.split.SchemalessSnapshotSplit;
+import
org.apache.flink.cdc.connectors.base.source.reader.IncrementalSourceReader;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.RowType;
+
+import org.apache.flink.shaded.guava31.com.google.common.collect.Lists;
+
+import io.debezium.relational.TableId;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests that the meta groups of finished snapshot splits stay consistent
across a checkpoint
+ * restore (FLINK-40216).
+ *
+ * <p>The meta groups served by {@code
IncrementalSourceEnumerator#sendStreamMetaRequestEvent} are
+ * partitioned from {@link SnapshotSplitAssigner#getFinishedSplitInfos()}, so
the assignment order
+ * of the splits must be retained across job restarts; otherwise a reader that
synchronized only
+ * part of the meta groups before a restart receives duplicated split infos
while never receiving
+ * others.
+ */
+class MetaGroupOrderingTest {
+
+ private static final TableId TABLE_ID = new TableId("db", null, "t");
+ private static final RowType SPLIT_KEY_TYPE =
+ new RowType(Collections.singletonList(new RowType.RowField("id",
new BigIntType())));
+
+ @Test
+ void testFinishedSplitInfosPreserveAssignmentOrderAfterRestore() {
+ List<String> assignmentOrder = new ArrayList<>();
+ SnapshotSplitAssigner<SourceConfig> restoredAssigner =
restoreAssigner(assignmentOrder, 12);
+
+ List<String> restoredOrder =
+ restoredAssigner.getFinishedSplitInfos().stream()
+ .map(FinishedSnapshotSplitInfo::getSplitId)
+ .collect(Collectors.toList());
+
+ assertThat(restoredOrder).isEqualTo(assignmentOrder);
+ }
+
+ @Test
+ void testReaderResumingAfterRestoreReceivesEachSplitExactlyOnce() {
+ int metaGroupSize = 2;
+ List<String> assignmentOrder = new ArrayList<>();
+ SnapshotSplitAssigner<SourceConfig> restoredAssigner =
restoreAssigner(assignmentOrder, 12);
+
+ // Before the restart the enumerator serves groups partitioned from
the assignment order;
+ // the reader synchronized groups 0..2 into its stream split state:
+ // [t:0, t:1], [t:2, t:3], [t:4, t:5]
+ List<List<String>> groupsBeforeRestart =
Lists.partition(assignmentOrder, metaGroupSize);
+ List<String> readerState = new ArrayList<>();
+ for (int groupId = 0; groupId < 3; groupId++) {
+ readerState.addAll(groupsBeforeRestart.get(groupId));
+ }
+
+ // The job restarts: the enumerator rebuilds its meta groups from the
restored assigner
+ List<List<FinishedSnapshotSplitInfo>> groupsAfterRestart =
+ Lists.partition(restoredAssigner.getFinishedSplitInfos(),
metaGroupSize);
+
+ // The reader resumes requesting group ids based on how many infos it
already holds
+ int nextGroupId =
+ IncrementalSourceReader.getNextMetaGroupId(readerState.size(),
metaGroupSize);
+ while (readerState.size() < assignmentOrder.size()
+ && nextGroupId < groupsAfterRestart.size()) {
+ for (FinishedSnapshotSplitInfo info :
groupsAfterRestart.get(nextGroupId)) {
+ readerState.add(info.getSplitId());
+ }
+ nextGroupId =
+
IncrementalSourceReader.getNextMetaGroupId(readerState.size(), metaGroupSize);
+ }
+
+ assertThat(readerState).containsExactlyElementsOf(assignmentOrder);
+ }
+
+ private static SnapshotSplitAssigner<SourceConfig> restoreAssigner(
+ List<String> assignmentOrder, int splitCount) {
+ LinkedHashMap<String, SchemalessSnapshotSplit> assignedSplits = new
LinkedHashMap<>();
+ Map<String, Offset> splitFinishedOffsets = new HashMap<>();
+ // 10+ splits so that the lexicographic order of split ids (t:0, t:1,
t:10, t:11, t:2,
+ // ...) differs from the assignment order (t:0, t:1, t:2, ..., t:11)
+ for (int i = 0; i < splitCount; i++) {
+ String splitId = TABLE_ID + ":" + i;
+ assignmentOrder.add(splitId);
+ assignedSplits.put(
+ splitId,
+ new SchemalessSnapshotSplit(
+ TABLE_ID,
+ splitId,
+ SPLIT_KEY_TYPE,
+ new Object[] {i},
+ new Object[] {i + 1},
+ null));
+ splitFinishedOffsets.put(splitId, null);
+ }
+
+ return new SnapshotSplitAssigner<>(
+ mock(SourceConfig.class),
+ 4,
+ new SnapshotPendingSplitsState(
+ Collections.singletonList(TABLE_ID),
+ Collections.emptyList(),
+ assignedSplits,
+ Collections.emptyMap(),
+ splitFinishedOffsets,
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ Collections.emptyList(),
+ false,
+ true,
+ Collections.emptyMap(),
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE),
+ mock(DataSourceDialect.class),
+ new MockOffsetFactory());
+ }
+
+ private static class MockOffsetFactory extends OffsetFactory {
+ @Override
+ public Offset newOffset(Map<String, String> offset) {
+ return null;
+ }
+
+ @Override
+ public Offset newOffset(String filename, Long position) {
+ return null;
+ }
+
+ @Override
+ public Offset newOffset(Long position) {
+ return null;
+ }
+
+ @Override
+ public Offset createTimestampOffset(long timestampMillis) {
+ return null;
+ }
+
+ @Override
+ public Offset createInitialOffset() {
+ return null;
+ }
+
+ @Override
+ public Offset createNoStoppingOffset() {
+ return null;
+ }
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializerTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializerTest.java
index c7bdb617d..2d5cf33e1 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializerTest.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/PendingSplitsStateSerializerTest.java
@@ -20,6 +20,7 @@ package
org.apache.flink.cdc.connectors.base.source.assigner.state;
import org.apache.flink.cdc.connectors.base.source.assigner.AssignerStatus;
import org.apache.flink.cdc.connectors.base.source.meta.offset.Offset;
import org.apache.flink.cdc.connectors.base.source.meta.offset.OffsetFactory;
+import
org.apache.flink.cdc.connectors.base.source.meta.split.SchemalessSnapshotSplit;
import org.apache.flink.cdc.connectors.base.source.meta.split.SnapshotSplit;
import
org.apache.flink.cdc.connectors.base.source.meta.split.SourceSplitSerializer;
import org.apache.flink.cdc.connectors.base.source.meta.split.StreamSplit;
@@ -38,6 +39,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -176,6 +179,54 @@ class PendingSplitsStateSerializerTest {
snapshotMetaReleased);
}
+ @Test
+ void testAssignedSplitsKeepAssignmentOrderAfterSerde() throws Exception {
+ TableId tableId = constructTableId();
+ RowType splitKeyType =
+ new RowType(
+ Collections.singletonList(new RowType.RowField("id",
new BigIntType())));
+ // 10+ splits so that the lexicographic order of split ids differs
from the assignment
+ // order, which determines the meta group partitioning of finished
snapshot splits
+ List<String> assignmentOrder = new ArrayList<>();
+ LinkedHashMap<String, SchemalessSnapshotSplit> assignedSplits = new
LinkedHashMap<>();
+ for (int i = 0; i < 12; i++) {
+ String splitId = tableId + ":" + i;
+ assignmentOrder.add(splitId);
+ assignedSplits.put(
+ splitId,
+ new SchemalessSnapshotSplit(
+ tableId,
+ splitId,
+ splitKeyType,
+ new Object[] {i},
+ new Object[] {i + 1},
+ null));
+ }
+ PendingSplitsStateSerializer serializer =
+ new
PendingSplitsStateSerializer(constructSourceSplitSerializer());
+ SnapshotPendingSplitsState state =
+ new SnapshotPendingSplitsState(
+ Collections.singletonList(tableId),
+ Collections.emptyList(),
+ assignedSplits,
+ constructTableSchema(),
+ Collections.emptyMap(),
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ Collections.emptyList(),
+ false,
+ true,
+ Collections.emptyMap(),
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE);
+
+ SnapshotPendingSplitsState restoredState =
+ (SnapshotPendingSplitsState)
+ serializer.deserialize(
+ serializer.getVersion(),
serializer.serialize(state));
+
+ assertThat(new ArrayList<>(restoredState.getAssignedSplits().keySet()))
+ .isEqualTo(assignmentOrder);
+ }
+
private SourceSplitSerializer constructSourceSplitSerializer() {
return new SourceSplitSerializer() {
@Override
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/SnapshotPendingSplitsStateTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/SnapshotPendingSplitsStateTest.java
new file mode 100644
index 000000000..40ee65480
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/assigner/state/SnapshotPendingSplitsStateTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.connectors.base.source.assigner.state;
+
+import org.apache.flink.cdc.connectors.base.source.assigner.AssignerStatus;
+import
org.apache.flink.cdc.connectors.base.source.meta.split.SchemalessSnapshotSplit;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.RowType;
+
+import io.debezium.relational.TableId;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/** Tests for {@link SnapshotPendingSplitsState}. */
+class SnapshotPendingSplitsStateTest {
+
+ /**
+ * Connectors compiled against earlier versions of flink-cdc-base link
against these exact
+ * method descriptors, so the parameter and return types must stay {@link
Map}.
+ */
+ @Test
+ void testPublicSignaturesKeepMapType() throws Exception {
+
assertThat(SnapshotPendingSplitsState.class.getMethod("getAssignedSplits").getReturnType())
+ .isEqualTo(Map.class);
+ assertThatCode(
+ () ->
+
SnapshotPendingSplitsState.class.getConstructor(
+ List.class,
+ List.class,
+ Map.class,
+ Map.class,
+ Map.class,
+ AssignerStatus.class,
+ List.class,
+ boolean.class,
+ boolean.class,
+ Map.class,
+ ChunkSplitterState.class))
+ .doesNotThrowAnyException();
+ assertThatCode(
+ () ->
+
SnapshotPendingSplitsState.class.getConstructor(
+ List.class,
+ List.class,
+ Map.class,
+ Map.class,
+ Map.class,
+ AssignerStatus.class,
+ List.class,
+ boolean.class,
+ boolean.class,
+ Map.class,
+ ChunkSplitterState.class,
+ boolean.class))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void testAssignedSplitsKeepIterationOrderOfGivenMap() {
+ TableId tableId = TableId.parse("catalog.schema.table");
+ RowType splitKeyType =
+ new RowType(
+ Collections.singletonList(new RowType.RowField("id",
new BigIntType())));
+ // not in lexicographic order of split ids
+ List<String> assignmentOrder =
+ Arrays.asList(tableId + ":2", tableId + ":10", tableId + ":1");
+ Map<String, SchemalessSnapshotSplit> assignedSplits = new
LinkedHashMap<>();
+ for (String splitId : assignmentOrder) {
+ assignedSplits.put(
+ splitId,
+ new SchemalessSnapshotSplit(tableId, splitId,
splitKeyType, null, null, null));
+ }
+
+ SnapshotPendingSplitsState state =
+ new SnapshotPendingSplitsState(
+ Collections.singletonList(tableId),
+ Collections.emptyList(),
+ assignedSplits,
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ Collections.emptyList(),
+ false,
+ true,
+ Collections.emptyMap(),
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE);
+
+ assertThat(new
ArrayList<>(state.getAssignedSplits().keySet())).isEqualTo(assignmentOrder);
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplitTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplitTest.java
new file mode 100644
index 000000000..116a6b90c
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/meta/split/StreamSplitTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.connectors.base.source.meta.split;
+
+import org.apache.flink.cdc.connectors.base.source.meta.offset.OffsetFactory;
+
+import io.debezium.relational.TableId;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/** Tests for {@link StreamSplit}. */
+class StreamSplitTest {
+
+ @Test
+ void testDuplicateFinishedSnapshotSplitInfosAreRejected() {
+ FinishedSnapshotSplitInfo info =
+ new FinishedSnapshotSplitInfo(
+ new TableId("catalog", "schema", "table"),
+ "split",
+ null,
+ null,
+ null,
+ mock(OffsetFactory.class));
+ List<FinishedSnapshotSplitInfo> infos = new ArrayList<>();
+ infos.add(info);
+ infos.add(info);
+
+ assertThatThrownBy(
+ () ->
+ new StreamSplit(
+ "stream-split",
+ null,
+ null,
+ infos,
+ Collections.emptyMap(),
+ 0,
+ false,
+ false))
+ .isExactlyInstanceOf(IllegalArgumentException.class);
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReaderTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReaderTest.java
index c9ea456fe..fbd4871ba 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReaderTest.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-cdc-base/src/test/java/org/apache/flink/cdc/connectors/base/source/reader/IncrementalSourceReaderTest.java
@@ -175,6 +175,62 @@ class IncrementalSourceReaderTest {
reader.close();
}
+ @Test
+ void testReaderDiscardsAlreadyReceivedPrefixOfPartiallyReceivedMetaGroup()
throws Exception {
+ // A reader restored in the middle of meta group synchronization holds
a partial last
+ // group. With a meta group size of 10, a reader holding 13 split
infos requests group 1
+ // (infos 10..19) and must discard the 3 infos it already holds. Split
ids are chosen so
+ // that their lexicographic order (":0, :1, :10, :11, :12, :2, ...")
differs from the
+ // assignment order.
+ TestingReaderContext readerContext = new TestingReaderContext();
+ TestingIncrementalSourceReader reader = createReader(readerContext);
+ reader.start();
+
+ List<FinishedSnapshotSplitInfo> receivedInfos = new ArrayList<>();
+ for (int i = 0; i < 13; i++) {
+ receivedInfos.add(finishedSplitInfo(i));
+ }
+ StreamSplit partialSplit =
+ new StreamSplit(
+ STREAM_SPLIT_ID,
+ new TestOffset(100L),
+ null,
+ receivedInfos,
+ new HashMap<>(),
+ 21);
+ reader.addSplits(Collections.singletonList(partialSplit));
+
+ List<byte[]> group1 = new ArrayList<>();
+ for (int i = 10; i < 20; i++) {
+ group1.add(finishedSplitInfo(i).serialize());
+ }
+ reader.handleSourceEvents(
+ new StreamSplitMetaEvent(partialSplit.splitId(), 1, group1,
21, 1));
+
+ // appendFinishedSplitInfos does not keep the order of the infos, so
only check that every
+ // split info is held exactly once
+ List<String> expectedSplitIds = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ expectedSplitIds.add(TABLE_ID + ":" + i);
+ }
+ assertThat(reader.getUncompletedStreamSplit(partialSplit.splitId()))
+ .extracting(StreamSplit::getFinishedSnapshotSplitInfos)
+ .asList()
+ .extracting(info -> ((FinishedSnapshotSplitInfo)
info).getSplitId())
+ .containsExactlyInAnyOrderElementsOf(expectedSplitIds);
+ reader.close();
+ }
+
+ private static FinishedSnapshotSplitInfo finishedSplitInfo(int chunkId) {
+ return new FinishedSnapshotSplitInfo(
+ TABLE_ID,
+ TABLE_ID + ":" + chunkId,
+ new Object[] {chunkId},
+ new Object[] {chunkId + 1},
+ new TestOffset(50L),
+ TEST_OFFSET_FACTORY);
+ }
+
private static TestingIncrementalSourceReader
createReader(TestingReaderContext readerContext) {
return new TestingIncrementalSourceReader(
new TrackingRecordEmitter(new
SourceReaderMetrics(readerContext.metricGroup())),
@@ -211,6 +267,10 @@ class IncrementalSourceReaderTest {
private SourceSplitState initialize(SourceSplitBase split) {
return initializedState(split);
}
+
+ private StreamSplit getUncompletedStreamSplit(String splitId) {
+ return uncompletedStreamSplits.get(splitId);
+ }
}
private static class TrackingRecordEmitter extends
IncrementalSourceRecordEmitter<Object> {