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 09d467466b [FLINK-39775][mysql] Release snapshot split metadata after
entering the stream phase (#4418)
09d467466b is described below
commit 09d467466bc24fa7afd3f45ce5f13a45f4c18614
Author: Spoorthi Basu <[email protected]>
AuthorDate: Wed Sep 16 21:44:04 2026 -0400
[FLINK-39775][mysql] Release snapshot split metadata after entering the
stream phase (#4418)
This closes #4418.
---
.../docs/connectors/flink-sources/mysql-cdc.md | 7 +
.../docs/connectors/pipeline-connectors/mysql.md | 7 +
.../docs/connectors/flink-sources/mysql-cdc.md | 7 +
.../docs/connectors/pipeline-connectors/mysql.md | 7 +
.../mysql/factory/MySqlDataSourceFactory.java | 5 +
.../mysql/source/MySqlDataSourceOptions.java | 8 +
.../cdc/connectors/mysql/source/MySqlSource.java | 6 +-
.../mysql/source/MySqlSourceBuilder.java | 11 +
.../source/assigners/MySqlHybridSplitAssigner.java | 113 +++-
.../assigners/MySqlSnapshotSplitAssigner.java | 34 +-
.../state/PendingSplitsStateSerializer.java | 38 +-
.../state/SnapshotPendingSplitsState.java | 43 +-
.../mysql/source/config/MySqlSourceConfig.java | 9 +-
.../source/config/MySqlSourceConfigFactory.java | 22 +-
.../mysql/source/config/MySqlSourceOptions.java | 15 +
.../source/enumerator/MySqlSourceEnumerator.java | 71 ++-
.../events/BinlogSplitMetaAssembledEvent.java | 56 ++
.../mysql/source/events/BinlogSplitMetaEvent.java | 15 +-
.../mysql/source/reader/MySqlSourceReader.java | 19 +
.../connectors/mysql/table/MySqlTableSource.java | 7 +
.../mysql/table/MySqlTableSourceFactory.java | 4 +
.../connectors/mysql/source/MySqlSourceITCase.java | 39 ++
.../MySqlSnapshotMetadataReleaseTest.java | 625 +++++++++++++++++++++
.../state/PendingSplitsStateSerializerTest.java | 119 +++-
.../enumerator/MySqlSourceEnumeratorTest.java | 66 +++
.../mysql/source/reader/MySqlSourceReaderTest.java | 103 ++++
.../mysql/table/MySqlTableSourceFactoryTest.java | 25 +
27 files changed, 1461 insertions(+), 20 deletions(-)
diff --git a/docs/content.zh/docs/connectors/flink-sources/mysql-cdc.md
b/docs/content.zh/docs/connectors/flink-sources/mysql-cdc.md
index 85b14141be..cbf7ec5b03 100644
--- a/docs/content.zh/docs/connectors/flink-sources/mysql-cdc.md
+++ b/docs/content.zh/docs/connectors/flink-sources/mysql-cdc.md
@@ -389,6 +389,13 @@ Flink SQL> SELECT * FROM orders;
For example: <code>'debezium.snapshot.mode' = 'never'</code>.
查看更多关于 <a
href="https://debezium.io/documentation/reference/1.9/connectors/mysql.html#mysql-connector-properties">
Debezium 的 MySQL 连接器属性</a></td>
</tr>
+ <tr>
+ <td>scan.incremental.snapshot.metadata.release.enabled</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>是否在 source 进入 binlog 阶段后,释放 source coordinator
持有的快照分片元数据(已分配的分片、已完成分片的位点以及表结构),以降低快照分片数量非常大的作业的 JobManager 内存占用。默认关闭。与
scan.newly-added-table.enabled
不兼容:同时开启两者会导致作业启动失败,且已释放元数据的作业无法再开启动态加表功能。仅在成功完成一次 checkpoint 后才会释放;若未开启
checkpoint 或没有 checkpoint 完成,则会保留该元数据,因此该配置项在未开启 checkpoint 时不生效。</td>
+ </tr>
<tr>
<td>scan.incremental.close-idle-reader.enabled</td>
<td>optional</td>
diff --git a/docs/content.zh/docs/connectors/pipeline-connectors/mysql.md
b/docs/content.zh/docs/connectors/pipeline-connectors/mysql.md
index 04ec284455..3efb4c63a9 100644
--- a/docs/content.zh/docs/connectors/pipeline-connectors/mysql.md
+++ b/docs/content.zh/docs/connectors/pipeline-connectors/mysql.md
@@ -282,6 +282,13 @@ pipeline:
<td>Boolean</td>
<td>是否启用动态加表特性,默认关闭。 此配置项只有作业从savepoint/checkpoint启动时才生效。</td>
</tr>
+ <tr>
+ <td>scan.incremental.snapshot.metadata.release.enabled</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>是否在进入 binlog 阶段后,释放 source coordinator 持有的快照分片元数据,以降低 JobManager
内存占用。与 scan.newly-added-table.enabled 不兼容(同时开启两者会导致作业启动失败);默认关闭。仅在成功完成一次
checkpoint 后才会释放;若未开启 checkpoint 或没有 checkpoint 完成,则会保留该元数据,因此该配置项在未开启
checkpoint 时不生效。</td>
+ </tr>
<tr>
<td>scan.binlog.newly-added-table.enabled</td>
<td>optional</td>
diff --git a/docs/content/docs/connectors/flink-sources/mysql-cdc.md
b/docs/content/docs/connectors/flink-sources/mysql-cdc.md
index 2d47d9b7d7..e857f7799b 100644
--- a/docs/content/docs/connectors/flink-sources/mysql-cdc.md
+++ b/docs/content/docs/connectors/flink-sources/mysql-cdc.md
@@ -387,6 +387,13 @@ Only valid for cdc 1.x version. During a snapshot
operation, the connector will
For example: <code>'debezium.snapshot.mode' = 'never'</code>.
See more about the <a
href="https://debezium.io/documentation/reference/1.9/connectors/mysql.html#mysql-connector-properties">Debezium's
MySQL Connector properties</a></td>
</tr>
+ <tr>
+ <td>scan.incremental.snapshot.metadata.release.enabled</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>Whether to release the snapshot split metadata (assigned splits,
finished offsets and table schemas) held by the source coordinator once the
source has entered the binlog phase, to reduce JobManager memory on jobs with a
very large number of snapshot splits. Disabled by default. Incompatible with
scan.newly-added-table.enabled: enabling both fails at startup, and a job that
has released the metadata cannot later enable newly-added-table scanning.
Release happens only after a su [...]
+ </tr>
<tr>
<td>scan.incremental.close-idle-reader.enabled</td>
<td>optional</td>
diff --git a/docs/content/docs/connectors/pipeline-connectors/mysql.md
b/docs/content/docs/connectors/pipeline-connectors/mysql.md
index e6e61e2072..bd8974a7ea 100644
--- a/docs/content/docs/connectors/pipeline-connectors/mysql.md
+++ b/docs/content/docs/connectors/pipeline-connectors/mysql.md
@@ -289,6 +289,13 @@ pipeline:
<td>Boolean</td>
<td>Whether to enable scan the newly added tables feature or not, by
default is false. This option is only useful when we start the job from a
savepoint/checkpoint.</td>
</tr>
+ <tr>
+ <td>scan.incremental.snapshot.metadata.release.enabled</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>Whether to release the snapshot split metadata held by the source
coordinator after entering the binlog phase to reduce JobManager memory.
Incompatible with scan.newly-added-table.enabled (enabling both fails at
startup); disabled by default. Release happens only after a successful
checkpoint; if checkpointing is disabled or no checkpoint completes, the
metadata is retained, so this option has no effect without checkpointing.</td>
+ </tr>
<tr>
<td>scan.binlog.newly-added-table.enabled</td>
<td>optional</td>
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/factory/MySqlDataSourceFactory.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/factory/MySqlDataSourceFactory.java
index 1b9cea5618..1dfc6194dc 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/factory/MySqlDataSourceFactory.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/factory/MySqlDataSourceFactory.java
@@ -80,6 +80,7 @@ import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOption
import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_BACKFILL_SKIP;
import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_CHUNK_KEY_COLUMN;
import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_CHUNK_SIZE;
+import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED;
import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_UNBOUNDED_CHUNK_FIRST_ENABLED;
import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_NEWLY_ADDED_TABLE_ENABLED;
import static
org.apache.flink.cdc.connectors.mysql.source.MySqlDataSourceOptions.SCAN_SNAPSHOT_FETCH_SIZE;
@@ -161,6 +162,8 @@ public class MySqlDataSourceFactory implements
DataSourceFactory {
int connectMaxRetries = config.get(CONNECT_MAX_RETRIES);
int connectionPoolSize = config.get(CONNECTION_POOL_SIZE);
boolean scanNewlyAddedTableEnabled =
config.get(SCAN_NEWLY_ADDED_TABLE_ENABLED);
+ boolean releaseSnapshotMetadataEnabled =
+ config.get(SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED);
boolean scanBinlogNewlyAddedTableEnabled =
config.get(SCAN_BINLOG_NEWLY_ADDED_TABLE_ENABLED);
boolean isParsingOnLineSchemaChanges =
config.get(PARSE_ONLINE_SCHEMA_CHANGES);
@@ -216,6 +219,7 @@ public class MySqlDataSourceFactory implements
DataSourceFactory {
.debeziumProperties(getDebeziumProperties(configMap))
.jdbcProperties(getJdbcProperties(configMap))
.scanNewlyAddedTableEnabled(scanNewlyAddedTableEnabled)
+
.releaseSnapshotMetadataEnabled(releaseSnapshotMetadataEnabled)
.parseOnLineSchemaChanges(isParsingOnLineSchemaChanges)
.treatTinyInt1AsBoolean(treatTinyInt1AsBoolean)
.useLegacyJsonFormat(useLegacyJsonFormat)
@@ -347,6 +351,7 @@ public class MySqlDataSourceFactory implements
DataSourceFactory {
options.add(SCAN_INCREMENTAL_CLOSE_IDLE_READER_ENABLED);
options.add(SCAN_INCREMENTAL_SNAPSHOT_CHUNK_KEY_COLUMN);
options.add(SCAN_NEWLY_ADDED_TABLE_ENABLED);
+ options.add(SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED);
options.add(CHUNK_META_GROUP_SIZE);
options.add(CHUNK_KEY_EVEN_DISTRIBUTION_FACTOR_UPPER_BOUND);
options.add(CHUNK_KEY_EVEN_DISTRIBUTION_FACTOR_LOWER_BOUND);
diff --git
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlDataSourceOptions.java
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlDataSourceOptions.java
index 6aff556e7f..20f7b0efa3 100644
---
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlDataSourceOptions.java
+++
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-mysql/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlDataSourceOptions.java
@@ -242,6 +242,14 @@ public class MySqlDataSourceOptions {
.withDescription(
"Whether to scan the newly added tables or not, by
default is false. This option is only useful when we start the job from a
savepoint/checkpoint.");
+ @Experimental
+ public static final ConfigOption<Boolean>
SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED =
+
ConfigOptions.key("scan.incremental.snapshot.metadata.release.enabled")
+ .booleanType()
+ .defaultValue(false)
+ .withDescription(
+ "Whether to release the snapshot split metadata
held by the source coordinator after entering the binlog phase to reduce
JobManager memory. Disabled by default. The release happens only after a
checkpoint completes, so it has no effect without checkpointing. Cannot be
enabled together with scan.newly-added-table.enabled (the job fails to
start).");
+
@Experimental
public static final ConfigOption<Boolean> SCHEMA_CHANGE_ENABLED =
ConfigOptions.key("schema-change.enabled")
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSource.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSource.java
index 5fcbae1a1e..3d25182edc 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSource.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSource.java
@@ -274,7 +274,11 @@ public class MySqlSource<T>
@Override
public SimpleVersionedSerializer<PendingSplitsState>
getEnumeratorCheckpointSerializer() {
- return new PendingSplitsStateSerializer(getSplitSerializer());
+ // Only write the v6 released flag when release is enabled. A
default-off job keeps the v5
+ // checkpoint format and stays restorable by an older connector build.
+ return new PendingSplitsStateSerializer(
+ getSplitSerializer(),
+
configFactory.createConfig(0).isReleaseSnapshotMetadataEnabled());
}
@Override
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceBuilder.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceBuilder.java
index 93fa2a0d36..ea40417ec0 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceBuilder.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceBuilder.java
@@ -211,6 +211,17 @@ public class MySqlSourceBuilder<T> {
return this;
}
+ /**
+ * Whether the source coordinator should release the snapshot split
metadata after entering the
+ * binlog phase to reduce JobManager memory. Incompatible with {@link
+ * #scanNewlyAddedTableEnabled(boolean)}; disabled by default.
+ */
+ public MySqlSourceBuilder<T> releaseSnapshotMetadataEnabled(
+ boolean releaseSnapshotMetadataEnabled) {
+
this.configFactory.releaseSnapshotMetadataEnabled(releaseSnapshotMetadataEnabled);
+ return this;
+ }
+
/** Specifies the startup options. */
public MySqlSourceBuilder<T> startupOptions(StartupOptions startupOptions)
{
this.configFactory.startupOptions(startupOptions);
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlHybridSplitAssigner.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlHybridSplitAssigner.java
index 4a25491b23..b81327820b 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlHybridSplitAssigner.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlHybridSplitAssigner.java
@@ -21,16 +21,20 @@ import
org.apache.flink.api.connector.source.SplitEnumeratorContext;
import
org.apache.flink.cdc.connectors.mysql.source.assigners.state.HybridPendingSplitsState;
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.events.BinlogSplitMetaAssembledEvent;
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.MySqlBinlogSplit;
import
org.apache.flink.cdc.connectors.mysql.source.split.MySqlSchemalessSnapshotSplit;
import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSplit;
+import org.apache.flink.util.Preconditions;
import io.debezium.relational.TableId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.annotation.Nullable;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -52,6 +56,20 @@ public class MySqlHybridSplitAssigner implements
MySqlSplitAssigner {
private boolean isBinlogSplitAssigned;
+ /** Whether the reader has reported holding the complete binlog split. */
+ private boolean binlogSplitMetaAssembled;
+
+ /** Checkpoint at which the metadata release was scheduled; released once
it completes. */
+ @Nullable private Long checkpointIdToReleaseMeta;
+
+ /**
+ * Generation of the current binlog split assignment, bumped on every
add-back. It rides on the
+ * meta groups served to the reader and comes back in the assembled event,
so a stale event from
+ * a failed attempt (older generation) can't arm the release for a fresh
reader. Not
+ * checkpointed: it resets to 0 on restore, which is fine because the
reader re-learns it.
+ */
+ private long binlogAssignmentGeneration;
+
private final MySqlSnapshotSplitAssigner snapshotSplitAssigner;
public MySqlHybridSplitAssigner(
@@ -97,6 +115,32 @@ public class MySqlHybridSplitAssigner implements
MySqlSplitAssigner {
this.snapshotSplitAssigner = snapshotSplitAssigner;
this.isBinlogSplitAssigned = isBinlogSplitAssigned;
this.splitMetaGroupSize = splitMetaGroupSize;
+ // Fail fast on an incompatible restore. A job that released its
snapshot metadata cannot
+ // later enable scan.newly-added-table, because the splits and schemas
that flow needs are
+ // gone. Reject it loudly instead of silently building an inconsistent
binlog split.
+ if (sourceConfig.isScanNewlyAddedTableEnabled()
+ && isBinlogSplitAssigned
+ && snapshotSplitAssigner.isSnapshotMetaReleased()) {
+ throw new IllegalStateException(
+ "scan.newly-added-table.enabled cannot be turned on for a
job that previously "
+ + "released its snapshot split metadata "
+ +
"(scan.incremental.snapshot.metadata.release.enabled=true). The "
+ + "assigned splits, finished offsets and table
schemas needed to capture "
+ + "newly added tables are no longer in state.
Start the job from a fresh "
+ + "state, or keep metadata release disabled if
newly-added-table scanning "
+ + "is required.");
+ }
+ // Fail fast if a job that already released its snapshot metadata is
restarted with the
+ // release option off. The metadata is gone from state, so falling
back to the v5 format
+ // would drop the released marker. Keep the option enabled or start
from a fresh state.
+ if (!sourceConfig.isReleaseSnapshotMetadataEnabled()
+ && snapshotSplitAssigner.isSnapshotMetaReleased()) {
+ throw new IllegalStateException(
+ "scan.incremental.snapshot.metadata.release.enabled cannot
be turned off for a "
+ + "job that already released its snapshot split
metadata. The assigned "
+ + "splits, finished offsets and table schemas are
no longer in state. "
+ + "Keep the option enabled, or start the job from
a fresh state.");
+ }
}
@Override
@@ -159,15 +203,39 @@ public class MySqlHybridSplitAssigner implements
MySqlSplitAssigner {
if (split.isSnapshotSplit()) {
snapshotSplits.add(split);
} else {
- // we don't store the split, but will re-create binlog split
later
+ // The binlog split is handed back for re-assignment; we don't
store it, but will
+ // re-create it later. Mark it unassigned and invalidate any
in-flight assembly.
isBinlogSplitAssigned = false;
+ onBinlogReaderReset();
}
}
snapshotSplitAssigner.addSplits(snapshotSplits);
}
+ /**
+ * Invalidates any in-flight binlog split assembly when the binlog reader
resets, so a stale
+ * assembled event from the failed attempt cannot arm a release. Also runs
when the split is not
+ * handed back, since a checkpoint-covered reset produces an empty
add-back.
+ */
+ public void onBinlogReaderReset() {
+ binlogSplitMetaAssembled = false;
+ checkpointIdToReleaseMeta = null;
+ binlogAssignmentGeneration++;
+ }
+
@Override
public PendingSplitsState snapshotState(long checkpointId) {
+ // Schedule the metadata release once the binlog split is assigned and
the reader has
+ // assembled it; it happens in notifyCheckpointComplete. Gated behind
the opt-in; the
+ // newly-added-table term is defensive, since createConfig already
rejects enabling both.
+ if (isBinlogSplitAssigned
+ && binlogSplitMetaAssembled
+ && checkpointIdToReleaseMeta == null
+ && !snapshotSplitAssigner.isSnapshotMetaReleased()
+ && sourceConfig.isReleaseSnapshotMetadataEnabled()
+ && !sourceConfig.isScanNewlyAddedTableEnabled()) {
+ checkpointIdToReleaseMeta = checkpointId;
+ }
return new HybridPendingSplitsState(
snapshotSplitAssigner.snapshotState(checkpointId),
isBinlogSplitAssigned);
}
@@ -175,6 +243,45 @@ public class MySqlHybridSplitAssigner implements
MySqlSplitAssigner {
@Override
public void notifyCheckpointComplete(long checkpointId) {
snapshotSplitAssigner.notifyCheckpointComplete(checkpointId);
+ // Release the snapshot metadata only after the checkpoint covering
the binlog split
+ // assignment completes. Doing it here (not in snapshotState) keeps
the assignment
+ // checkpoint-covered, so addSplitsBack can never return the split to
an emptied assigner.
+ if (checkpointIdToReleaseMeta != null
+ && checkpointId >= checkpointIdToReleaseMeta
+ && !snapshotSplitAssigner.isSnapshotMetaReleased()) {
+ snapshotSplitAssigner.releaseSnapshotMetadata();
+ }
+ }
+
+ /**
+ * Marks that the reader holds the complete binlog split, arming the
metadata release. A
+ * reported generation that is neither the current one nor the inline
sentinel is ignored, which
+ * drops a stale event from a reader attempt that an add-back has already
superseded.
+ */
+ public void onBinlogSplitMetaAssembled(long reportedGeneration) {
+ // An inline reader requests no meta groups, so it reports
COMPLETE_WITHOUT_META_GENERATION;
+ // honor that regardless, since it never needs the coordinator's
metadata. Otherwise require
+ // an exact match, which drops a stale event from a failed attempt
superseded by an
+ // add-back.
+ if (reportedGeneration !=
BinlogSplitMetaAssembledEvent.COMPLETE_WITHOUT_META_GENERATION
+ && reportedGeneration != binlogAssignmentGeneration) {
+ LOG.info(
+ "Ignoring binlog split assembled event for generation {};
current generation is {}.",
+ reportedGeneration,
+ binlogAssignmentGeneration);
+ return;
+ }
+ this.binlogSplitMetaAssembled = true;
+ }
+
+ /** Returns the current binlog split assignment generation. */
+ public long getBinlogAssignmentGeneration() {
+ return binlogAssignmentGeneration;
+ }
+
+ /** Returns whether the snapshot split metadata has been released. */
+ public boolean isSnapshotMetaReleased() {
+ return snapshotSplitAssigner.isSnapshotMetaReleased();
}
@Override
@@ -205,6 +312,10 @@ public class MySqlHybridSplitAssigner implements
MySqlSplitAssigner {
//
--------------------------------------------------------------------------------------------
private MySqlBinlogSplit createBinlogSplit() {
+ Preconditions.checkState(
+ !snapshotSplitAssigner.isSnapshotMetaReleased(),
+ "Snapshot metadata was already released; the binlog split must
not be re-created "
+ + "after that.");
final List<MySqlSchemalessSnapshotSplit> assignedSnapshotSplit =
new
ArrayList<>(snapshotSplitAssigner.getAssignedSplits().values());
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotSplitAssigner.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotSplitAssigner.java
index f51df018d4..8f8794b4c7 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotSplitAssigner.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotSplitAssigner.java
@@ -108,6 +108,9 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
@Nullable private Long checkpointIdToFinish;
+ /** Whether the snapshot split metadata has been released. */
+ private boolean snapshotMetaReleased;
+
public MySqlSnapshotSplitAssigner(
MySqlSourceConfig sourceConfig,
int currentParallelism,
@@ -127,6 +130,7 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
isTableIdCaseSensitive,
true,
ChunkSplitterState.NO_SPLITTING_TABLE_STATE,
+ false,
enumeratorContext);
}
@@ -148,6 +152,7 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
checkpoint.isTableIdCaseSensitive(),
checkpoint.isRemainingTablesCheckpointed(),
checkpoint.getChunkSplitterState(),
+ checkpoint.isSnapshotMetaReleased(),
enumeratorContext);
}
@@ -164,6 +169,7 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
boolean isTableIdCaseSensitive,
boolean isRemainingTablesCheckpointed,
ChunkSplitterState chunkSplitterState,
+ boolean snapshotMetaReleased,
SplitEnumeratorContext<MySqlSplit> enumeratorContext) {
this.sourceConfig = sourceConfig;
this.currentParallelism = currentParallelism;
@@ -181,6 +187,9 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
this.partition =
new
MySqlPartition(sourceConfig.getMySqlConnectorConfig().getLogicalName());
this.enumeratorContext = enumeratorContext;
+ // The released flag is persisted in the checkpoint
(PendingSplitsStateSerializer v6), so
+ // restore it directly rather than inferring it from the state shape.
+ this.snapshotMetaReleased = snapshotMetaReleased;
}
@Override
@@ -462,7 +471,8 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
remainingTables,
isTableIdCaseSensitive,
true,
- chunkSplitter.snapshotState(checkpointId));
+ chunkSplitter.snapshotState(checkpointId),
+ snapshotMetaReleased);
// we need a complete checkpoint before mark this assigner to be
finished, to wait for
// all records of snapshot splits are completely processed
if (checkpointIdToFinish == null
@@ -571,6 +581,28 @@ public class MySqlSnapshotSplitAssigner implements
MySqlSplitAssigner {
return splitFinishedOffsets;
}
+ /**
+ * Releases the heavyweight snapshot split metadata (assigned splits,
finished offsets, table
+ * schemas) after the binlog phase begins, so it is no longer held in the
coordinator nor
+ * checkpointed. Keeps {@link #alreadyProcessedTables} and the assigner
status so a restore does
+ * not re-discover tables. Only called once the binlog split is assigned
and checkpoint-covered.
+ */
+ public void releaseSnapshotMetadata() {
+ if (snapshotMetaReleased) {
+ return;
+ }
+ assignedSplits.clear();
+ splitFinishedOffsets.clear();
+ tableSchemas.clear();
+ snapshotMetaReleased = true;
+ LOG.info("Released snapshot split metadata after entering the binlog
phase.");
+ }
+
+ /** Returns whether {@link #releaseSnapshotMetadata()} has already been
performed. */
+ public boolean isSnapshotMetaReleased() {
+ return snapshotMetaReleased;
+ }
+
//
-------------------------------------------------------------------------------------------
/**
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializer.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializer.java
index 0df1ee9799..212b9a8c24 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializer.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializer.java
@@ -45,8 +45,13 @@ import java.util.Map;
*/
public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<PendingSplitsState> {
- // TODO: need proper implementation of the new version
- private static final int VERSION = 5;
+ // Checkpoint format version. Version 6 (FLINK-39775) additionally
persists the
+ // snapshot-metadata released flag, and is written only when metadata
release is enabled. A job
+ // that leaves the option off keeps writing version 5, so it can still be
restored by an older
+ // connector build that does not understand the flag.
+ private static final int VERSION_WITHOUT_RELEASED_FLAG = 5;
+ private static final int VERSION_WITH_RELEASED_FLAG = 6;
+
private static final ThreadLocal<DataOutputSerializer> SERIALIZER_CACHE =
ThreadLocal.withInitial(() -> new DataOutputSerializer(64));
@@ -55,19 +60,27 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
private static final int HYBRID_PENDING_SPLITS_STATE_FLAG = 3;
private final SimpleVersionedSerializer<MySqlSplit> splitSerializer;
+ private final boolean releaseSnapshotMetadataEnabled;
- public PendingSplitsStateSerializer(SimpleVersionedSerializer<MySqlSplit>
splitSerializer) {
+ public PendingSplitsStateSerializer(
+ SimpleVersionedSerializer<MySqlSplit> splitSerializer,
+ boolean releaseSnapshotMetadataEnabled) {
this.splitSerializer = splitSerializer;
+ this.releaseSnapshotMetadataEnabled = releaseSnapshotMetadataEnabled;
}
@Override
public int getVersion() {
- return VERSION;
+ return releaseSnapshotMetadataEnabled
+ ? VERSION_WITH_RELEASED_FLAG
+ : VERSION_WITHOUT_RELEASED_FLAG;
}
@Override
public byte[] serialize(PendingSplitsState state) throws IOException {
- // optimization: the splits lazily cache their own serialized form
+ // optimization: the splits lazily cache their own serialized form. A
job serializes with a
+ // single serializer whose released-flag setting is fixed, so the
cached payload never mixes
+ // the v5 and v6 forms; the cache is transient and starts null after a
restore.
if (state.serializedFormCache != null) {
return state.serializedFormCache;
}
@@ -110,6 +123,7 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
case 3:
case 4:
case 5:
+ case 6:
return deserializePendingSplitsState(version, serialized);
default:
throw new IOException("Unknown version: " + version);
@@ -176,6 +190,11 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
new Object[]
{chunkSplitterState.getNextChunkStart().getValue()}));
out.writeInt(chunkSplitterState.getNextChunkId());
}
+ // v6 (FLINK-39775): the released flag is written only when release is
enabled. A job with
+ // the option off keeps the v5 format and stays restorable by an older
connector build.
+ if (releaseSnapshotMetadataEnabled) {
+ out.writeBoolean(state.isSnapshotMetaReleased());
+ }
}
private void serializeHybridPendingSplitsState(
@@ -299,6 +318,12 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
nextChunkId = in.readInt();
}
}
+ // v6 (FLINK-39775): the released flag is persisted; older versions
never released, so it
+ // defaults to false.
+ boolean snapshotMetaReleased = false;
+ if (version >= 6) {
+ snapshotMetaReleased = in.readBoolean();
+ }
return new SnapshotPendingSplitsState(
alreadyProcessedTables,
remainingSchemalessSplits,
@@ -314,7 +339,8 @@ public class PendingSplitsStateSerializer implements
SimpleVersionedSerializer<P
: new ChunkSplitterState(
splittingTableId,
ChunkSplitterState.ChunkBound.middleOf(nextChunkStart),
- nextChunkId));
+ nextChunkId),
+ snapshotMetaReleased);
}
private HybridPendingSplitsState deserializeHybridPendingSplitsState(
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/SnapshotPendingSplitsState.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/SnapshotPendingSplitsState.java
index 55df2e7daf..28ff7b73ee 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/SnapshotPendingSplitsState.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/SnapshotPendingSplitsState.java
@@ -75,6 +75,9 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
/** The data structure to record the state of a {@link ChunkSplitter}. */
private final ChunkSplitterState chunkSplitterState;
+ /** Whether the heavyweight snapshot split metadata has been released
(FLINK-39775). */
+ private final boolean snapshotMetaReleased;
+
public SnapshotPendingSplitsState(
List<TableId> alreadyProcessedTables,
List<MySqlSchemalessSnapshotSplit> remainingSplits,
@@ -86,6 +89,32 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
boolean isTableIdCaseSensitive,
boolean isRemainingTablesCheckpointed,
ChunkSplitterState chunkSplitterState) {
+ this(
+ alreadyProcessedTables,
+ remainingSplits,
+ assignedSplits,
+ tableSchemas,
+ splitFinishedOffsets,
+ assignerStatus,
+ remainingTables,
+ isTableIdCaseSensitive,
+ isRemainingTablesCheckpointed,
+ chunkSplitterState,
+ false);
+ }
+
+ public SnapshotPendingSplitsState(
+ List<TableId> alreadyProcessedTables,
+ List<MySqlSchemalessSnapshotSplit> remainingSplits,
+ LinkedHashMap<String, MySqlSchemalessSnapshotSplit> assignedSplits,
+ Map<TableId, TableChange> tableSchemas,
+ Map<String, BinlogOffset> splitFinishedOffsets,
+ AssignerStatus assignerStatus,
+ List<TableId> remainingTables,
+ boolean isTableIdCaseSensitive,
+ boolean isRemainingTablesCheckpointed,
+ ChunkSplitterState chunkSplitterState,
+ boolean snapshotMetaReleased) {
// FLINK-38061: make defensive copy to avoid potential concurrent
modification of the
// collections.
this.alreadyProcessedTables = new ArrayList<>(alreadyProcessedTables);
@@ -98,6 +127,7 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
this.isRemainingTablesCheckpointed = isRemainingTablesCheckpointed;
this.tableSchemas = new HashMap<>(tableSchemas);
this.chunkSplitterState = chunkSplitterState;
+ this.snapshotMetaReleased = snapshotMetaReleased;
}
public List<TableId> getAlreadyProcessedTables() {
@@ -140,6 +170,11 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
return chunkSplitterState;
}
+ /** Whether the heavyweight snapshot split metadata has been released
(FLINK-39775). */
+ public boolean isSnapshotMetaReleased() {
+ return snapshotMetaReleased;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -157,7 +192,8 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
&& Objects.equals(remainingSplits, that.remainingSplits)
&& Objects.equals(assignedSplits, that.assignedSplits)
&& Objects.equals(splitFinishedOffsets,
that.splitFinishedOffsets)
- && Objects.equals(chunkSplitterState, that.chunkSplitterState);
+ && Objects.equals(chunkSplitterState, that.chunkSplitterState)
+ && snapshotMetaReleased == that.snapshotMetaReleased;
}
@Override
@@ -171,7 +207,8 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
assignerStatus,
isTableIdCaseSensitive,
isRemainingTablesCheckpointed,
- chunkSplitterState);
+ chunkSplitterState,
+ snapshotMetaReleased);
}
@Override
@@ -195,6 +232,8 @@ public class SnapshotPendingSplitsState extends
PendingSplitsState {
+ isRemainingTablesCheckpointed
+ ", chunkSplitterState="
+ chunkSplitterState
+ + ", snapshotMetaReleased="
+ + snapshotMetaReleased
+ '}';
}
}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfig.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfig.java
index 6b87dfef9a..85e1cb55e4 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfig.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfig.java
@@ -73,6 +73,7 @@ public class MySqlSourceConfig implements Serializable {
private final boolean parseOnLineSchemaChanges;
public static boolean useLegacyJsonFormat = true;
private final boolean assignUnboundedChunkFirst;
+ private final boolean releaseSnapshotMetadataEnabled;
//
--------------------------------------------------------------------------------------------
// Debezium Configurations
@@ -113,7 +114,8 @@ public class MySqlSourceConfig implements Serializable {
boolean parseOnLineSchemaChanges,
boolean treatTinyInt1AsBoolean,
boolean useLegacyJsonFormat,
- boolean assignUnboundedChunkFirst) {
+ boolean assignUnboundedChunkFirst,
+ boolean releaseSnapshotMetadataEnabled) {
this.hostname = checkNotNull(hostname);
this.port = port;
this.username = checkNotNull(username);
@@ -161,6 +163,7 @@ public class MySqlSourceConfig implements Serializable {
this.treatTinyInt1AsBoolean = treatTinyInt1AsBoolean;
this.useLegacyJsonFormat = useLegacyJsonFormat;
this.assignUnboundedChunkFirst = assignUnboundedChunkFirst;
+ this.releaseSnapshotMetadataEnabled = releaseSnapshotMetadataEnabled;
}
public String getHostname() {
@@ -248,6 +251,10 @@ public class MySqlSourceConfig implements Serializable {
return scanNewlyAddedTableEnabled;
}
+ public boolean isReleaseSnapshotMetadataEnabled() {
+ return releaseSnapshotMetadataEnabled;
+ }
+
public boolean isCloseIdleReaders() {
return closeIdleReaders;
}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfigFactory.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfigFactory.java
index 569b62232d..495acac4d9 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfigFactory.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceConfigFactory.java
@@ -37,6 +37,7 @@ import java.util.UUID;
import static
org.apache.flink.cdc.connectors.mysql.source.utils.EnvironmentUtils.checkSupportCheckpointsAfterTasksFinished;
import static org.apache.flink.util.Preconditions.checkNotNull;
+import static org.apache.flink.util.Preconditions.checkState;
/** A factory to construct {@link MySqlSourceConfig}. */
@Internal
@@ -68,6 +69,7 @@ public class MySqlSourceConfigFactory implements Serializable
{
private boolean includeHeartbeatEvents = false;
private boolean includeTransactionMetadataEvents = false;
private boolean scanNewlyAddedTableEnabled = false;
+ private boolean releaseSnapshotMetadataEnabled = false;
private boolean closeIdleReaders = false;
private Properties jdbcProperties;
private Duration heartbeatInterval =
MySqlSourceOptions.HEARTBEAT_INTERVAL.defaultValue();
@@ -258,6 +260,17 @@ public class MySqlSourceConfigFactory implements
Serializable {
return this;
}
+ /**
+ * Whether the source coordinator should release the snapshot split
metadata after entering the
+ * binlog phase to reduce JobManager memory. Incompatible with {@link
+ * #scanNewlyAddedTableEnabled(boolean)}; disabled by default.
+ */
+ public MySqlSourceConfigFactory releaseSnapshotMetadataEnabled(
+ boolean releaseSnapshotMetadataEnabled) {
+ this.releaseSnapshotMetadataEnabled = releaseSnapshotMetadataEnabled;
+ return this;
+ }
+
/** Custom properties that will overwrite the default JDBC connection URL.
*/
public MySqlSourceConfigFactory jdbcProperties(Properties jdbcProperties) {
this.jdbcProperties = jdbcProperties;
@@ -355,6 +368,12 @@ public class MySqlSourceConfigFactory implements
Serializable {
/** Creates a new {@link MySqlSourceConfig} for the given subtask {@code
subtaskId}. */
public MySqlSourceConfig createConfig(int subtaskId, String serverName) {
checkSupportCheckpointsAfterTasksFinished(closeIdleReaders);
+ checkState(
+ !(scanNewlyAddedTableEnabled &&
releaseSnapshotMetadataEnabled),
+ "scan.incremental.snapshot.metadata.release.enabled and "
+ + "scan.newly-added-table.enabled cannot both be
enabled: releasing the "
+ + "snapshot split metadata would drop the assigned
splits, finished offsets "
+ + "and table schemas that newly-added-table scanning
needs.");
Properties props = new Properties();
props.setProperty("database.server.name", serverName);
props.setProperty("database.hostname", checkNotNull(hostname));
@@ -444,6 +463,7 @@ public class MySqlSourceConfigFactory implements
Serializable {
parseOnLineSchemaChanges,
treatTinyInt1AsBoolean,
useLegacyJsonFormat,
- assignUnboundedChunkFirst);
+ assignUnboundedChunkFirst,
+ releaseSnapshotMetadataEnabled);
}
}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceOptions.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceOptions.java
index a8e143f5fc..8a12a0c9e7 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceOptions.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/config/MySqlSourceOptions.java
@@ -243,6 +243,21 @@ public class MySqlSourceOptions {
.withDescription(
"Whether capture the scan the newly added tables
or not, by default is false. This option is only useful when we start the job
from a savepoint/checkpoint.");
+ @Experimental
+ public static final ConfigOption<Boolean>
SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED =
+
ConfigOptions.key("scan.incremental.snapshot.metadata.release.enabled")
+ .booleanType()
+ .defaultValue(false)
+ .withDescription(
+ "Whether to release the snapshot split metadata
(assigned splits, finished"
+ + " offsets and table schemas) held by the
source coordinator once the"
+ + " source has entered the binlog phase,
to reduce JobManager memory"
+ + " pressure on jobs with a very large
number of snapshot splits. Disabled"
+ + " by default. The release happens only
after a checkpoint completes, so it has no"
+ + " effect without checkpointing. Cannot
be enabled together with"
+ + " scan.newly-added-table.enabled (the
job fails to start), since that"
+ + " flow needs the metadata that would be
released.");
+
@Experimental
public static final ConfigOption<String>
SCAN_INCREMENTAL_SNAPSHOT_CHUNK_KEY_COLUMN =
ConfigOptions.key("scan.incremental.snapshot.chunk.key-column")
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 0451db76aa..ff0279e4aa 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
@@ -27,6 +27,7 @@ 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.events.BinlogSplitAssignedEvent;
+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.BinlogSplitMetaRequestEvent;
import
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitUpdateAckEvent;
@@ -126,6 +127,17 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
if (binlogSplit.isPresent()) {
LOG.info("The enumerator adds add binlog split back: {}",
binlogSplit);
this.binlogSplitTaskId = null;
+ // The split is coming back to the coordinator; addSplits below
resets the assembly.
+ } else if (binlogSplitTaskId != null
+ && binlogSplitTaskId.equals(subtaskId)
+ && splitAssigner instanceof MySqlHybridSplitAssigner) {
+ // The binlog holder reset but handed nothing back (a
checkpoint-covered reset the
+ // reader restores itself), so addSplits will not see it;
invalidate the assembly here.
+ LOG.info(
+ "The binlog reader on subtask {} reset without handing its
split back; "
+ + "invalidating any assembled binlog split
metadata.",
+ subtaskId);
+ ((MySqlHybridSplitAssigner) splitAssigner).onBinlogReaderReset();
}
if (!CollectionUtil.isNullOrEmpty(splits)) {
splitAssigner.addSplits(splits);
@@ -179,7 +191,28 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
LOG.info(
"The enumerator receives notice from subtask {} for the
binlog split assignment. ",
subtaskId);
- binlogSplitTaskId = subtaskId;
+ // Only learn the holder from this event when it is unknown (e.g.
after a coordinator
+ // restart). A real reassignment sets it via assignSplits, so an
event naming a
+ // different subtask than the known holder is stale and must not
overwrite it.
+ if (binlogSplitTaskId == null) {
+ binlogSplitTaskId = subtaskId;
+ } else if (!binlogSplitTaskId.equals(subtaskId)) {
+ LOG.info(
+ "Ignoring a stale binlog split assigned event from
subtask {}; the binlog "
+ + "split is currently held by subtask {}.",
+ subtaskId,
+ binlogSplitTaskId);
+ }
+ } else if (sourceEvent instanceof BinlogSplitMetaAssembledEvent) {
+ LOG.info(
+ "The enumerator receives notice from subtask {} that the
binlog split metadata has been fully assembled. ",
+ subtaskId);
+ if (splitAssigner instanceof MySqlHybridSplitAssigner) {
+ ((MySqlHybridSplitAssigner) splitAssigner)
+ .onBinlogSplitMetaAssembled(
+ ((BinlogSplitMetaAssembledEvent) sourceEvent)
+ .getBinlogAssignmentGeneration());
+ }
}
}
@@ -191,6 +224,13 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
@Override
public void notifyCheckpointComplete(long checkpointId) {
splitAssigner.notifyCheckpointComplete(checkpointId);
+ // Once the assigner has released the snapshot metadata, drop the
enumerator's cached binlog
+ // split meta groups too: it is a second copy of the same
finished-split infos.
+ if (binlogSplitMeta != null
+ && splitAssigner instanceof MySqlHybridSplitAssigner
+ && ((MySqlHybridSplitAssigner)
splitAssigner).isSnapshotMetaReleased()) {
+ binlogSplitMeta = null;
+ }
// binlog split may be available after checkpoint complete
assignSplits();
}
@@ -291,6 +331,19 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
}
private void sendBinlogMeta(int subTask, BinlogSplitMetaRequestEvent
requestEvent) {
+ // After the snapshot metadata has been released, the finished-split
infos are gone. A meta
+ // request arriving now can only come from a failed reader attempt: a
live reader that has
+ // assembled the binlog split recovers it from its own checkpointed
state and never
+ // re-requests. Ignore it instead of rebuilding from an emptied
assigner, which would throw
+ // FlinkRuntimeException and fail the job.
+ if (splitAssigner instanceof MySqlHybridSplitAssigner
+ && ((MySqlHybridSplitAssigner)
splitAssigner).isSnapshotMetaReleased()) {
+ LOG.info(
+ "Ignoring a stale binlog split meta request from subtask
{} received after the "
+ + "snapshot metadata was released.",
+ subTask);
+ return;
+ }
// initialize once
if (binlogSplitMeta == null) {
final List<FinishedSnapshotSplitInfo> finishedSnapshotSplitInfos =
@@ -319,7 +372,8 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
requestEvent.getSplitId(),
requestMetaGroupId,
null,
- totalFinishedSplitSizeOfEnumerator);
+ totalFinishedSplitSizeOfEnumerator,
+ currentBinlogAssignmentGeneration());
context.sendEventToSourceReader(subTask, metadataEvent);
} else if (binlogSplitMeta.size() > requestMetaGroupId) {
List<FinishedSnapshotSplitInfo> metaToSend =
binlogSplitMeta.get(requestMetaGroupId);
@@ -330,7 +384,8 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
metaToSend.stream()
.map(FinishedSnapshotSplitInfo::serialize)
.collect(Collectors.toList()),
- totalFinishedSplitSizeOfEnumerator);
+ totalFinishedSplitSizeOfEnumerator,
+ currentBinlogAssignmentGeneration());
context.sendEventToSourceReader(subTask, metadataEvent);
} else {
throw new FlinkRuntimeException(
@@ -343,6 +398,16 @@ public class MySqlSourceEnumerator implements
SplitEnumerator<MySqlSplit, Pendin
}
}
+ /**
+ * The current binlog split assignment generation, or 0 when not in hybrid
mode. Stamped onto
+ * the meta groups served to a reader so a stale assembled event can be
filtered out.
+ */
+ private long currentBinlogAssignmentGeneration() {
+ return splitAssigner instanceof MySqlHybridSplitAssigner
+ ? ((MySqlHybridSplitAssigner)
splitAssigner).getBinlogAssignmentGeneration()
+ : 0L;
+ }
+
private void handleLatestFinishedSplitNumberRequest(int subTask) {
if (splitAssigner instanceof MySqlHybridSplitAssigner) {
context.sendEventToSourceReader(
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaAssembledEvent.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaAssembledEvent.java
new file mode 100644
index 0000000000..79a902caa1
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaAssembledEvent.java
@@ -0,0 +1,56 @@
+/*
+ * 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.mysql.source.events;
+
+import org.apache.flink.api.connector.source.SourceEvent;
+import
org.apache.flink.cdc.connectors.mysql.source.enumerator.MySqlSourceEnumerator;
+import org.apache.flink.cdc.connectors.mysql.source.reader.MySqlSourceReader;
+import org.apache.flink.cdc.connectors.mysql.source.split.MySqlBinlogSplit;
+
+/**
+ * The {@link SourceEvent} that {@link MySqlSourceReader} sends to {@link
MySqlSourceEnumerator}
+ * once it holds a complete {@link MySqlBinlogSplit}, signalling that all
finished snapshot split
+ * metadata has been assembled by the reader. This lets the enumerator release
the snapshot split
+ * metadata it retains. Idempotent: re-sent whenever a complete binlog split
(re-)enters reading.
+ *
+ * <p>Carries the generation the reader assembled under, or {@link
+ * #COMPLETE_WITHOUT_META_GENERATION} for an inline split. The coordinator
uses it to drop a stale
+ * event left over from a failed reader attempt, which would otherwise arm the
release before a
+ * freshly-assigned reader has re-assembled.
+ */
+public class BinlogSplitMetaAssembledEvent implements SourceEvent {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Reported instead of a real generation when the reader held a complete
split inline and never
+ * requested meta groups. The coordinator releases on it regardless of
generation, since an
+ * inline reader never needs the coordinator's metadata.
+ */
+ public static final long COMPLETE_WITHOUT_META_GENERATION = -1L;
+
+ private final long binlogAssignmentGeneration;
+
+ public BinlogSplitMetaAssembledEvent(long binlogAssignmentGeneration) {
+ this.binlogAssignmentGeneration = binlogAssignmentGeneration;
+ }
+
+ public long getBinlogAssignmentGeneration() {
+ return binlogAssignmentGeneration;
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaEvent.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaEvent.java
index bdd553a73a..02bf1f5a4a 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaEvent.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/source/events/BinlogSplitMetaEvent.java
@@ -48,15 +48,24 @@ public class BinlogSplitMetaEvent implements SourceEvent {
private final int totalFinishedSplitSize;
+ /**
+ * The binlog assignment generation current at the coordinator when this
meta group was served.
+ * The reader echoes it back in {@link BinlogSplitMetaAssembledEvent} so
the coordinator can
+ * tell a fresh assembled signal from a stale one left by a failed reader
attempt.
+ */
+ private final long binlogAssignmentGeneration;
+
public BinlogSplitMetaEvent(
String splitId,
int metaGroupId,
@Nullable List<byte[]> metaGroup,
- int totalFinishedSplitSize) {
+ int totalFinishedSplitSize,
+ long binlogAssignmentGeneration) {
this.splitId = splitId;
this.metaGroupId = metaGroupId;
this.metaGroup = metaGroup;
this.totalFinishedSplitSize = totalFinishedSplitSize;
+ this.binlogAssignmentGeneration = binlogAssignmentGeneration;
}
public String getSplitId() {
@@ -74,4 +83,8 @@ public class BinlogSplitMetaEvent implements SourceEvent {
public int getTotalFinishedSplitSize() {
return totalFinishedSplitSize;
}
+
+ public long getBinlogAssignmentGeneration() {
+ return binlogAssignmentGeneration;
+ }
}
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 b3f421ceec..4d6f4a8620 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
@@ -22,6 +22,7 @@ import
org.apache.flink.cdc.common.annotation.VisibleForTesting;
import org.apache.flink.cdc.connectors.mysql.debezium.DebeziumUtils;
import org.apache.flink.cdc.connectors.mysql.source.config.MySqlSourceConfig;
import
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitAssignedEvent;
+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.BinlogSplitMetaRequestEvent;
import
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitUpdateAckEvent;
@@ -81,6 +82,16 @@ public class MySqlSourceReader<T>
private final MySqlRecordEmitter<T> recordEmitter;
private final MySqlPartition partition;
+ /**
+ * Generation the reader learned from its served meta groups, echoed in
{@link
+ * BinlogSplitMetaAssembledEvent} so the coordinator can drop a stale
signal from a failed
+ * attempt. Stays {@link
BinlogSplitMetaAssembledEvent#COMPLETE_WITHOUT_META_GENERATION} for an
+ * inline split that requested none. Not checkpointed: on restart it
resets to that sentinel,
+ * which is safe because a reader reports the sentinel only while it holds
a complete split.
+ */
+ private long binlogAssignmentGeneration =
+ BinlogSplitMetaAssembledEvent.COMPLETE_WITHOUT_META_GENERATION;
+
public MySqlSourceReader(
Supplier<MySqlSplitReader> splitReaderSupplier,
MySqlRecordEmitter<T> recordEmitter,
@@ -285,6 +296,11 @@ public class MySqlSourceReader<T>
discoverTableSchemasForBinlogSplit(
binlogSplit, sourceConfig,
checkNewlyAddedTableSchema);
unfinishedSplits.add(mySqlBinlogSplit);
+ // binlog split is complete: tell the enumerator so it can
release the snapshot
+ // metadata. Also fires on restore of a complete split,
which is fine. The
+ // generation ties this signal to the current assignment.
+ context.sendSourceEventToCoordinator(
+ new
BinlogSplitMetaAssembledEvent(binlogAssignmentGeneration));
}
LOG.info(
"Source reader {} received the binlog split : {}.",
subtaskId, binlogSplit);
@@ -400,6 +416,9 @@ public class MySqlSourceReader<T>
private void fillMetadataForBinlogSplit(BinlogSplitMetaEvent
metadataEvent) {
MySqlBinlogSplit binlogSplit =
uncompletedBinlogSplits.get(metadataEvent.getSplitId());
if (binlogSplit != null) {
+ // Track the generation this metadata was served under, so the
assembled event we send
+ // once the split is complete is tied to the current assignment.
+ this.binlogAssignmentGeneration =
metadataEvent.getBinlogAssignmentGeneration();
final int receivedMetaGroupId = metadataEvent.getMetaGroupId();
final int receivedTotalFinishedSplitSize =
metadataEvent.getTotalFinishedSplitSize();
final int expectedMetaGroupId =
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSource.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSource.java
index 2a1f051943..d44d0d4fe7 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSource.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSource.java
@@ -93,6 +93,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
private final double distributionFactorLower;
private final StartupOptions startupOptions;
private final boolean scanNewlyAddedTableEnabled;
+ private final boolean releaseSnapshotMetadataEnabled;
private final boolean closeIdleReaders;
private final Properties jdbcProperties;
private final Duration heartbeatInterval;
@@ -136,6 +137,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
double distributionFactorLower,
StartupOptions startupOptions,
boolean scanNewlyAddedTableEnabled,
+ boolean releaseSnapshotMetadataEnabled,
boolean closeIdleReaders,
Properties jdbcProperties,
Duration heartbeatInterval,
@@ -166,6 +168,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
this.distributionFactorLower = distributionFactorLower;
this.startupOptions = startupOptions;
this.scanNewlyAddedTableEnabled = scanNewlyAddedTableEnabled;
+ this.releaseSnapshotMetadataEnabled = releaseSnapshotMetadataEnabled;
this.closeIdleReaders = closeIdleReaders;
this.jdbcProperties = jdbcProperties;
this.parseOnlineSchemaChanges = parseOnlineSchemaChanges;
@@ -233,6 +236,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
.startupOptions(startupOptions)
.deserializer(deserializer)
.scanNewlyAddedTableEnabled(scanNewlyAddedTableEnabled)
+
.releaseSnapshotMetadataEnabled(releaseSnapshotMetadataEnabled)
.closeIdleReaders(closeIdleReaders)
.jdbcProperties(jdbcProperties)
.heartbeatInterval(heartbeatInterval)
@@ -322,6 +326,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
distributionFactorLower,
startupOptions,
scanNewlyAddedTableEnabled,
+ releaseSnapshotMetadataEnabled,
closeIdleReaders,
jdbcProperties,
heartbeatInterval,
@@ -353,6 +358,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
&& distributionFactorUpper == that.distributionFactorUpper
&& distributionFactorLower == that.distributionFactorLower
&& scanNewlyAddedTableEnabled ==
that.scanNewlyAddedTableEnabled
+ && releaseSnapshotMetadataEnabled ==
that.releaseSnapshotMetadataEnabled
&& closeIdleReaders == that.closeIdleReaders
&& Objects.equals(physicalSchema, that.physicalSchema)
&& Objects.equals(hostname, that.hostname)
@@ -405,6 +411,7 @@ public class MySqlTableSource implements ScanTableSource,
SupportsReadingMetadat
producedDataType,
metadataKeys,
scanNewlyAddedTableEnabled,
+ releaseSnapshotMetadataEnabled,
closeIdleReaders,
jdbcProperties,
heartbeatInterval,
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactory.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactory.java
index 5ea430d94e..e3fd290ff1 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactory.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/main/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactory.java
@@ -90,6 +90,8 @@ public class MySqlTableSourceFactory implements
DynamicTableSourceFactory {
config.get(MySqlSourceOptions.CHUNK_KEY_EVEN_DISTRIBUTION_FACTOR_LOWER_BOUND);
boolean scanNewlyAddedTableEnabled =
config.get(MySqlSourceOptions.SCAN_NEWLY_ADDED_TABLE_ENABLED);
+ boolean releaseSnapshotMetadataEnabled =
+
config.get(MySqlSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED);
Duration heartbeatInterval =
config.get(MySqlSourceOptions.HEARTBEAT_INTERVAL);
String chunkKeyColumn =
config.getOptional(MySqlSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_CHUNK_KEY_COLUMN)
@@ -148,6 +150,7 @@ public class MySqlTableSourceFactory implements
DynamicTableSourceFactory {
distributionFactorLower,
startupOptions,
scanNewlyAddedTableEnabled,
+ releaseSnapshotMetadataEnabled,
closeIdleReaders,
JdbcUrlUtils.getJdbcProperties(context.getCatalogTable().getOptions()),
heartbeatInterval,
@@ -198,6 +201,7 @@ public class MySqlTableSourceFactory implements
DynamicTableSourceFactory {
options.add(MySqlSourceOptions.CHUNK_KEY_EVEN_DISTRIBUTION_FACTOR_LOWER_BOUND);
options.add(MySqlSourceOptions.CONNECT_MAX_RETRIES);
options.add(MySqlSourceOptions.SCAN_NEWLY_ADDED_TABLE_ENABLED);
+
options.add(MySqlSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_METADATA_RELEASE_ENABLED);
options.add(MySqlSourceOptions.SCAN_INCREMENTAL_CLOSE_IDLE_READER_ENABLED);
options.add(MySqlSourceOptions.HEARTBEAT_INTERVAL);
options.add(MySqlSourceOptions.SCAN_INCREMENTAL_SNAPSHOT_CHUNK_KEY_COLUMN);
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceITCase.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceITCase.java
index 62f51e4951..aa29e49f1e 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceITCase.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/MySqlSourceITCase.java
@@ -293,6 +293,45 @@ class MySqlSourceITCase extends MySqlSourceTestBase {
assignEndingFirst));
}
+ @ParameterizedTest
+ @MethodSource("parameters")
+ void testTaskManagerFailoverInBinlogPhaseWithMetadataRelease(
+ String tableName, String chunkColumnName, String
assignEndingFirst) throws Exception {
+ // Releasing the snapshot metadata after entering the binlog phase
must not lose or
+ // duplicate binlog data across a reader (TaskManager) failover. The
released metadata is
+ // checkpoint-covered, so the binlog split is recovered from the
reader's own state rather
+ // than re-created from the emptied assigner.
+ Map<String, String> options = new HashMap<>();
+ options.put("scan.incremental.snapshot.unbounded-chunk-first.enabled",
assignEndingFirst);
+ options.put("scan.incremental.snapshot.metadata.release.enabled",
"true");
+ testMySqlParallelSource(
+ FailoverType.TM,
+ FailoverPhase.BINLOG,
+ new String[] {tableName, "customers_1"},
+ tableName,
+ chunkColumnName,
+ options);
+ }
+
+ @ParameterizedTest
+ @MethodSource("parameters")
+ void testJobManagerFailoverInBinlogPhaseWithMetadataRelease(
+ String tableName, String chunkColumnName, String
assignEndingFirst) throws Exception {
+ // Same as above but across a JobManager (coordinator) failover, which
restores the
+ // enumerator from a released "light" checkpoint and must not
re-discover tables or rebuild
+ // the binlog split from empty metadata.
+ Map<String, String> options = new HashMap<>();
+ options.put("scan.incremental.snapshot.unbounded-chunk-first.enabled",
assignEndingFirst);
+ options.put("scan.incremental.snapshot.metadata.release.enabled",
"true");
+ testMySqlParallelSource(
+ FailoverType.JM,
+ FailoverPhase.BINLOG,
+ new String[] {tableName, "customers_1"},
+ tableName,
+ chunkColumnName,
+ options);
+ }
+
@ParameterizedTest
@MethodSource("parameters")
void testTaskManagerFailoverFromLatestOffset(
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotMetadataReleaseTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotMetadataReleaseTest.java
new file mode 100644
index 0000000000..475e1520a1
--- /dev/null
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/MySqlSnapshotMetadataReleaseTest.java
@@ -0,0 +1,625 @@
+/*
+ * 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.mysql.source.assigners;
+
+import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.cdc.connectors.mysql.MySqlTestUtils;
+import org.apache.flink.cdc.connectors.mysql.source.MySqlSource;
+import
org.apache.flink.cdc.connectors.mysql.source.assigners.state.ChunkSplitterState;
+import
org.apache.flink.cdc.connectors.mysql.source.assigners.state.HybridPendingSplitsState;
+import
org.apache.flink.cdc.connectors.mysql.source.assigners.state.PendingSplitsStateSerializer;
+import
org.apache.flink.cdc.connectors.mysql.source.assigners.state.SnapshotPendingSplitsState;
+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.enumerator.MySqlSourceEnumerator;
+import
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitAssignedEvent;
+import
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitMetaAssembledEvent;
+import
org.apache.flink.cdc.connectors.mysql.source.events.BinlogSplitMetaRequestEvent;
+import org.apache.flink.cdc.connectors.mysql.source.offset.BinlogOffset;
+import org.apache.flink.cdc.connectors.mysql.source.split.MySqlBinlogSplit;
+import
org.apache.flink.cdc.connectors.mysql.source.split.MySqlSchemalessSnapshotSplit;
+import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSplit;
+import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSplitSerializer;
+import
org.apache.flink.cdc.connectors.mysql.source.utils.MockMySqlSplitEnumeratorEnumeratorContext;
+import org.apache.flink.cdc.connectors.mysql.table.StartupOptions;
+import org.apache.flink.table.api.DataTypes;
+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.apache.kafka.connect.source.SourceRecord;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZoneId;
+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.Optional;
+
+import static
org.apache.flink.cdc.connectors.mysql.testutils.MetricsUtils.getMySqlSplitEnumeratorContext;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for releasing the heavyweight snapshot split metadata in {@link
MySqlHybridSplitAssigner}
+ * after the source has safely entered the binlog phase.
+ *
+ * <p>These tests restore the assigner from a finished snapshot checkpoint, so
they exercise the
+ * whole release protocol purely in memory without needing a running MySQL
instance.
+ */
+class MySqlSnapshotMetadataReleaseTest {
+
+ private static final String DB = "release_test_db";
+ private static final String TABLE = "customers";
+ private static final int NUM_FINISHED_SPLITS = 5;
+
+ @Test
+ void testReleaseAfterMetaAssembledAndCheckpointComplete() {
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+
+ // enter the binlog phase
+
assertThat(assigner.getNext()).isPresent().get().isInstanceOf(MySqlBinlogSplit.class);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 1, NUM_FINISHED_SPLITS);
+
+ // reader reports it has assembled the complete binlog split metadata
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+
+ // snapshotState schedules the release but does not clear yet
+ assigner.snapshotState(2L);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 3, NUM_FINISHED_SPLITS);
+
+ // the covering checkpoint completes -> release happens now
+ assigner.notifyCheckpointComplete(2L);
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ HybridPendingSplitsState state = (HybridPendingSplitsState)
assigner.snapshotState(4L);
+
assertThat(state.getSnapshotPendingSplits().getAssignedSplits()).isEmpty();
+
assertThat(state.getSnapshotPendingSplits().getSplitFinishedOffsets()).isEmpty();
+
assertThat(state.getSnapshotPendingSplits().getTableSchemas()).isEmpty();
+ // bookkeeping that must be kept so a restore does not re-discover
tables
+
assertThat(state.getSnapshotPendingSplits().getAlreadyProcessedTables()).isNotEmpty();
+ assertThat(state.isBinlogSplitAssigned()).isTrue();
+
+ assigner.close();
+ }
+
+ @Test
+ void testNoReleaseWithoutMetaAssembledAck() {
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ assigner.getNext();
+
+ // no onBinlogSplitMetaAssembled() call -> never release
+ assigner.snapshotState(1L);
+ assigner.notifyCheckpointComplete(1L);
+
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 2, NUM_FINISHED_SPLITS);
+ assigner.close();
+ }
+
+ @Test
+ void testNoReleaseBeforeCheckpointComplete() {
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ assigner.getNext();
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+
+ // schedule release at checkpoint 5, but complete an earlier
checkpoint only
+ assigner.snapshotState(5L);
+ assigner.notifyCheckpointComplete(4L);
+
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 6, NUM_FINISHED_SPLITS);
+ assigner.close();
+ }
+
+ @Test
+ void testFailFastWhenBothOptionsEnabled() {
+ // Enabling both metadata release and scan.newly-added-table is
contradictory: the release
+ // would drop the metadata the newly-added-table flow needs. Reject it
at config build time
+ // rather than silently disabling the release.
+ assertThatThrownBy(() -> buildConfig(true, true))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("cannot both be enabled");
+ }
+
+ @Test
+ void testNoReleaseWhenReleaseMetadataDisabled() {
+ // The release optimization is opt-in. With it disabled (the default)
the metadata is
+ // retained exactly as before this change, so a job can still enable
scan.newly-added-table
+ // later. This guards against the regression raised in review.
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false, false);
+ assigner.getNext();
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+
+ assigner.snapshotState(1L);
+ assigner.notifyCheckpointComplete(1L);
+
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 2, NUM_FINISHED_SPLITS);
+ assigner.close();
+ }
+
+ @Test
+ void testAddBackResetsScheduledReleaseAndKeepsMetadata() {
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ MySqlSplit binlogSplit = assigner.getNext().get();
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+ assigner.snapshotState(1L); // schedules release at checkpoint 1
+
+ // the binlog split is added back (reader failover before the release
checkpoint completes)
+ assigner.addSplits(Collections.singletonList(binlogSplit));
+
+ // completing checkpoint 1 must NOT release, because the schedule was
reset on add-back
+ assigner.notifyCheckpointComplete(1L);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 2, NUM_FINISHED_SPLITS);
+
+ // and the binlog split can be recreated with the full metadata intact
+ Optional<MySqlSplit> recreated = assigner.getNext();
+
assertThat(recreated).isPresent().get().isInstanceOf(MySqlBinlogSplit.class);
+ assertThat(recreated.get().asBinlogSplit().getTotalFinishedSplitSize())
+ .isEqualTo(NUM_FINISHED_SPLITS);
+ assigner.close();
+ }
+
+ @Test
+ void testStaleAssembledEventFromOldGenerationDoesNotRelease() {
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ MySqlSplit binlogSplit = assigner.getNext().get();
+
+ // the first reader assembles under generation 0 and reports it
+ long staleGeneration = assigner.getBinlogAssignmentGeneration();
+ assigner.onBinlogSplitMetaAssembled(staleGeneration);
+ assigner.snapshotState(1L); // schedules release at checkpoint 1
+
+ // the reader fails: the binlog split is added back, which bumps the
generation
+ assigner.addSplits(Collections.singletonList(binlogSplit));
+
assertThat(assigner.getBinlogAssignmentGeneration()).isGreaterThan(staleGeneration);
+
+ // the binlog split is re-created for a fresh reader that has not
assembled yet
+
assertThat(assigner.getNext()).isPresent().get().isInstanceOf(MySqlBinlogSplit.class);
+
+ // a stale assembled event from the failed attempt (old generation)
arrives late. It must be
+ // ignored, otherwise the metadata would be released while the new
reader still needs it.
+ assigner.onBinlogSplitMetaAssembled(staleGeneration);
+ assigner.snapshotState(2L);
+ assigner.notifyCheckpointComplete(2L);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assertAssignedSplitsSize(assigner, 3, NUM_FINISHED_SPLITS);
+
+ // once the freshly-assigned reader reports under the current
generation, release proceeds
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+ assigner.snapshotState(4L);
+ assigner.notifyCheckpointComplete(4L);
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ assigner.close();
+ }
+
+ @Test
+ void testInlineSplitStillReleasesAfterAddBackBumpedGeneration() {
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ MySqlSplit binlogSplit = assigner.getNext().get();
+
+ // a binlog split failover bumps the generation past the initial 0
+ assigner.addSplits(Collections.singletonList(binlogSplit));
+ assertThat(assigner.getBinlogAssignmentGeneration()).isGreaterThan(0L);
+
assertThat(assigner.getNext()).isPresent().get().isInstanceOf(MySqlBinlogSplit.class);
+
+ // an inline reader never requests meta groups, so it never learns the
bumped generation
+ // and reports COMPLETE_WITHOUT_META_GENERATION. Release must still
happen, otherwise the
+ // optimization silently stops working for small tables after any
failover.
+ assigner.onBinlogSplitMetaAssembled(
+
BinlogSplitMetaAssembledEvent.COMPLETE_WITHOUT_META_GENERATION);
+ assigner.snapshotState(2L);
+ assigner.notifyCheckpointComplete(2L);
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ assigner.close();
+ }
+
+ @Test
+ void testEmptyAddBackFromBinlogReaderInvalidatesStaleAssembledEvent() {
+ // A reader holding the binlog split can reset without its split being
handed back: when the
+ // assignment is already checkpoint-covered, Flink restores the split
from the reader's own
+ // state and calls addSplitsBack with an empty list. The generation
must still be bumped and
+ // any scheduled release cancelled, otherwise a delayed assembled
event from the failed
+ // attempt would arm an unsafe release while the restored reader still
needs the metadata.
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ assigner.getNext(); // enter the binlog phase; the binlog split is
assigned
+
+ MySqlSourceEnumerator enumerator =
+ new MySqlSourceEnumerator(
+ getMySqlSplitEnumeratorContext(),
+ buildConfig(false, true),
+ assigner,
+ Boundedness.CONTINUOUS_UNBOUNDED);
+
+ // subtask 0 reports it holds the binlog split, then assembles it and
schedules a release
+ enumerator.handleSourceEvent(0, new BinlogSplitAssignedEvent());
+ long staleGeneration = assigner.getBinlogAssignmentGeneration();
+ assigner.onBinlogSplitMetaAssembled(staleGeneration);
+ assigner.snapshotState(1L); // schedules release at checkpoint 1
+
+ // subtask 0 resets, but its assignment was checkpoint-covered, so
Flink hands nothing back
+ enumerator.addSplitsBack(Collections.emptyList(), 0);
+
+ // completing checkpoint 1 must NOT release: the reset cancelled the
schedule
+ assigner.notifyCheckpointComplete(1L);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+
assertThat(assigner.getBinlogAssignmentGeneration()).isGreaterThan(staleGeneration);
+
+ // a delayed assembled event from the failed attempt (old generation)
is now ignored
+ assigner.onBinlogSplitMetaAssembled(staleGeneration);
+ assigner.snapshotState(2L);
+ assigner.notifyCheckpointComplete(2L);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+
+ assigner.close();
+ }
+
+ @Test
+ void testStaleBinlogSplitAssignedEventDoesNotCorruptHolder() {
+ // A delayed BinlogSplitAssignedEvent from a failed attempt must not
overwrite the live
+ // binlog holder. If it did, the empty-add-back reset detection (which
keys off
+ // binlogSplitTaskId) would be defeated and a stale assembled event
could arm an unsafe
+ // release. Here the real holder is subtask 1; a stale assigned event
from subtask 0 arrives
+ // and must be ignored, so the later empty add-back from subtask 1
still cancels the
+ // release.
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ assigner.getNext(); // enter the binlog phase
+
+ MySqlSourceEnumerator enumerator =
+ new MySqlSourceEnumerator(
+ getMySqlSplitEnumeratorContext(),
+ buildConfig(false, true),
+ assigner,
+ Boundedness.CONTINUOUS_UNBOUNDED);
+
+ // subtask 1 is the live binlog holder; a stale assigned event from
subtask 0 must be
+ // ignored
+ enumerator.handleSourceEvent(1, new BinlogSplitAssignedEvent());
+ enumerator.handleSourceEvent(0, new BinlogSplitAssignedEvent());
+
+ long staleGeneration = assigner.getBinlogAssignmentGeneration();
+ assigner.onBinlogSplitMetaAssembled(staleGeneration);
+ assigner.snapshotState(1L); // schedules release at checkpoint 1
+
+ // the real holder (subtask 1) resets with an empty add-back; because
binlogSplitTaskId was
+ // not corrupted to 0, the reset is still detected and the scheduled
release is cancelled
+ enumerator.addSplitsBack(Collections.emptyList(), 1);
+ assigner.notifyCheckpointComplete(1L);
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+
assertThat(assigner.getBinlogAssignmentGeneration()).isGreaterThan(staleGeneration);
+
+ assigner.close();
+ }
+
+ @Test
+ void testRestoreFromReleasedLightState() {
+ MySqlHybridSplitAssigner assigner = buildReleasedLightAssigner();
+
+ // No binlog split is re-created and no tables are re-discovered after
a light restore.
+ assertThat(assigner.getNext()).isEmpty();
+ // The released flag is restored from the persisted checkpoint value,
so after restore the
+ // assigner knows the metadata was already released.
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ // The state stays light (empty heavy maps, binlog split still
assigned).
+ HybridPendingSplitsState state = (HybridPendingSplitsState)
assigner.snapshotState(10L);
+
assertThat(state.getSnapshotPendingSplits().getAssignedSplits()).isEmpty();
+
assertThat(state.getSnapshotPendingSplits().getSplitFinishedOffsets()).isEmpty();
+
assertThat(state.getSnapshotPendingSplits().getAlreadyProcessedTables()).isNotEmpty();
+ assertThat(state.isBinlogSplitAssigned()).isTrue();
+
+ // Already released after restore; re-sending the assembled event and
completing a
+ // checkpoint is a no-op, and the assigner stays in the released state.
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+ assigner.snapshotState(11L);
+ assigner.notifyCheckpointComplete(11L);
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ assigner.close();
+ }
+
+ @Test
+ void testReleasedFlagNotInferredWhenSnapshotSplitsRemain() {
+ // The released flag is persisted (PendingSplitsStateSerializer v6),
not inferred from state
+ // shape. A finished parallelism-1 state that never released (flag
defaults false) must
+ // restore as not-released even with non-empty remainingSplits and
empty assignedSplits.
+ // This guards against anyone re-introducing shape-based inference,
which previously
+ // false-positived here and crash-looped createBinlogSplit,
feature-off jobs included.
+ List<TableId> alreadyProcessedTables = Lists.newArrayList(new
TableId(null, DB, TABLE));
+ RowType splitKeyType =
+ (RowType) DataTypes.ROW(DataTypes.FIELD("id",
DataTypes.BIGINT())).getLogicalType();
+ List<MySqlSchemalessSnapshotSplit> remainingSplits = new ArrayList<>();
+ remainingSplits.add(
+ new MySqlSchemalessSnapshotSplit(
+ new TableId(null, DB, TABLE),
+ DB + "." + TABLE + ":0",
+ splitKeyType,
+ null,
+ new Object[] {2},
+ BinlogOffset.ofBinlogFilePosition("mysql-bin.00001",
1)));
+ SnapshotPendingSplitsState snapshotState =
+ new SnapshotPendingSplitsState(
+ alreadyProcessedTables,
+ remainingSplits,
+ new LinkedHashMap<>(),
+ new HashMap<>(),
+ new HashMap<>(),
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ new ArrayList<>(),
+ false,
+ true,
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE);
+ HybridPendingSplitsState checkpoint = new
HybridPendingSplitsState(snapshotState, false);
+ MySqlHybridSplitAssigner assigner =
+ new MySqlHybridSplitAssigner(
+ buildConfig(false, true), 1, checkpoint,
getMySqlSplitEnumeratorContext());
+
+ assertThat(assigner.isSnapshotMetaReleased()).isFalse();
+ assigner.close();
+ }
+
+ @Test
+ void testFailFastWhenNewlyAddedEnabledOnReleasedRestore() {
+ // A job that released its metadata cannot later enable
scan.newly-added-table, because the
+ // metadata that flow needs is gone. Restoring such a state with the
flag on must fail fast
+ // with a clear error rather than silently corrupting the binlog split.
+ assertThatThrownBy(() -> buildReleasedLightAssigner(true, false))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("scan.newly-added-table.enabled cannot
be turned on");
+ }
+
+ @Test
+ void testFailFastWhenReleaseDisabledOnReleasedRestore() {
+ // A job that already released its snapshot metadata cannot be
restarted with the release
+ // option turned off. The metadata is gone from state, and writing the
lighter v5 format
+ // would drop the released marker, so restoring such a state with the
option off must fail
+ // fast rather than silently corrupt the binlog split.
+ assertThatThrownBy(() -> buildReleasedLightAssigner(false, false))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining(
+ "scan.incremental.snapshot.metadata.release.enabled
cannot be turned off");
+ }
+
+ @Test
+ void testEnumeratorSerializerWritesV6OnlyWhenReleaseEnabled() {
+ // The real wiring: MySqlSource.getEnumeratorCheckpointSerializer()
picks the serializer
+ // version from the option, so a release-enabled job writes v6 and a
default-off job keeps
+ // writing v5.
+
assertThat(buildSource(true).getEnumeratorCheckpointSerializer().getVersion()).isEqualTo(6);
+
assertThat(buildSource(false).getEnumeratorCheckpointSerializer().getVersion())
+ .isEqualTo(5);
+ }
+
+ @Test
+ void testReleasedStateSerializedThenRestoredWithOptionOffFailsFast()
throws Exception {
+ // End to end: a released state serialized by the release-on
serializer (v6, flag true),
+ // then
+ // restored with the option off, must deserialize the flag as true and
fail fast rather than
+ // silently dropping the marker by rewriting the v5 format.
+ HybridPendingSplitsState released = buildReleasedHybridCheckpoint();
+ PendingSplitsStateSerializer serializer =
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, true);
+ byte[] bytes = serializer.serialize(released);
+ HybridPendingSplitsState restored =
+ (HybridPendingSplitsState)
serializer.deserialize(serializer.getVersion(), bytes);
+
assertThat(restored.getSnapshotPendingSplits().isSnapshotMetaReleased()).isTrue();
+
+ assertThatThrownBy(
+ () ->
+ new MySqlHybridSplitAssigner(
+ buildConfig(false, false),
+ 4,
+ restored,
+ getMySqlSplitEnumeratorContext()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining(
+ "scan.incremental.snapshot.metadata.release.enabled
cannot be turned off");
+ }
+
+ @Test
+ void testStaleBinlogMetaRequestAfterLightRestoreIsIgnored() {
+ // After restoring from a released light checkpoint the released flag
is restored from the
+ // persisted checkpoint value, so the enumerator still ignores a stray
meta request instead
+ // of rebuilding and throwing.
+ MySqlHybridSplitAssigner assigner = buildReleasedLightAssigner();
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ MySqlSourceEnumerator enumerator =
+ new MySqlSourceEnumerator(
+ getMySqlSplitEnumeratorContext(),
+ buildConfig(false, true),
+ assigner,
+ Boundedness.CONTINUOUS_UNBOUNDED);
+
+ assertThatCode(
+ () ->
+ enumerator.handleSourceEvent(
+ 0,
+ new BinlogSplitMetaRequestEvent(
+ "binlog-split", 0,
NUM_FINISHED_SPLITS)))
+ .doesNotThrowAnyException();
+
+ assigner.close();
+ }
+
+ @Test
+ void testStaleBinlogMetaRequestAfterReleaseIsIgnored() {
+ // A stale BinlogSplitMetaRequestEvent from a failed reader attempt
can arrive after the
+ // snapshot metadata has been released. The enumerator must ignore it
instead of rebuilding
+ // from the emptied assigner, which would throw FlinkRuntimeException
and fail the job.
+ MySqlHybridSplitAssigner assigner =
buildFinishedSnapshotAssigner(false);
+ assigner.getNext();
+
assigner.onBinlogSplitMetaAssembled(assigner.getBinlogAssignmentGeneration());
+ assigner.snapshotState(1L);
+ assigner.notifyCheckpointComplete(1L);
+ assertThat(assigner.isSnapshotMetaReleased()).isTrue();
+
+ MySqlSourceEnumerator enumerator =
+ new MySqlSourceEnumerator(
+ getMySqlSplitEnumeratorContext(),
+ buildConfig(false, true),
+ assigner,
+ Boundedness.CONTINUOUS_UNBOUNDED);
+
+ assertThatCode(
+ () ->
+ enumerator.handleSourceEvent(
+ 0,
+ new BinlogSplitMetaRequestEvent(
+ "binlog-split", 0,
NUM_FINISHED_SPLITS)))
+ .doesNotThrowAnyException();
+
+ assigner.close();
+ }
+
+ //
------------------------------------------------------------------------------------------
+
+ private void assertAssignedSplitsSize(
+ MySqlHybridSplitAssigner assigner, long checkpointId, int
expectedSize) {
+ HybridPendingSplitsState state =
+ (HybridPendingSplitsState)
assigner.snapshotState(checkpointId);
+
assertThat(state.getSnapshotPendingSplits().getAssignedSplits()).hasSize(expectedSize);
+ assertThat(state.getSnapshotPendingSplits().getSplitFinishedOffsets())
+ .hasSize(expectedSize);
+ }
+
+ private MySqlSourceConfig buildConfig(
+ boolean scanNewlyAddedTableEnabled, boolean
releaseSnapshotMetadataEnabled) {
+ return new MySqlSourceConfigFactory()
+ .startupOptions(StartupOptions.initial())
+ .databaseList(DB)
+ .tableList(DB + "." + TABLE)
+ .hostname("localhost")
+ .port(3306)
+ .username("root")
+ .password("")
+ .scanNewlyAddedTableEnabled(scanNewlyAddedTableEnabled)
+ .releaseSnapshotMetadataEnabled(releaseSnapshotMetadataEnabled)
+ .serverTimeZone(ZoneId.of("UTC").toString())
+ .createConfig(0);
+ }
+
+ private MySqlHybridSplitAssigner buildFinishedSnapshotAssigner(
+ boolean scanNewlyAddedTableEnabled) {
+ return buildFinishedSnapshotAssigner(scanNewlyAddedTableEnabled, true);
+ }
+
+ private MySqlHybridSplitAssigner buildFinishedSnapshotAssigner(
+ boolean scanNewlyAddedTableEnabled, boolean
releaseSnapshotMetadataEnabled) {
+ MySqlSourceConfig config =
+ buildConfig(scanNewlyAddedTableEnabled,
releaseSnapshotMetadataEnabled);
+
+ TableId tableId = new TableId(null, DB, TABLE);
+ RowType splitKeyType =
+ (RowType) DataTypes.ROW(DataTypes.FIELD("id",
DataTypes.BIGINT())).getLogicalType();
+
+ List<TableId> alreadyProcessedTables = Lists.newArrayList(tableId);
+ LinkedHashMap<String, MySqlSchemalessSnapshotSplit> assignedSplits =
new LinkedHashMap<>();
+ Map<String, BinlogOffset> splitFinishedOffsets = new HashMap<>();
+ for (int i = 0; i < NUM_FINISHED_SPLITS; i++) {
+ String splitId = DB + "." + TABLE + ":" + i;
+ Object[] splitStart = i == 0 ? null : new Object[] {i * 2};
+ Object[] splitEnd = new Object[] {i * 2 + 2};
+ BinlogOffset highWatermark =
+ BinlogOffset.ofBinlogFilePosition("mysql-bin.00001", i +
1);
+ assignedSplits.put(
+ splitId,
+ new MySqlSchemalessSnapshotSplit(
+ tableId, splitId, splitKeyType, splitStart,
splitEnd, highWatermark));
+ splitFinishedOffsets.put(splitId, highWatermark);
+ }
+
+ SnapshotPendingSplitsState snapshotState =
+ new SnapshotPendingSplitsState(
+ alreadyProcessedTables,
+ new ArrayList<>(),
+ assignedSplits,
+ new HashMap<>(),
+ splitFinishedOffsets,
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ new ArrayList<>(),
+ false,
+ true,
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE);
+ HybridPendingSplitsState checkpoint = new
HybridPendingSplitsState(snapshotState, false);
+ MockMySqlSplitEnumeratorEnumeratorContext enumeratorContext =
+ getMySqlSplitEnumeratorContext();
+ return new MySqlHybridSplitAssigner(config, 4, checkpoint,
enumeratorContext);
+ }
+
+ /**
+ * Builds an assigner restored from a "light" checkpoint: the metadata was
already released, so
+ * the maps are empty, the binlog split is assigned, and only
alreadyProcessedTables and the
+ * finished status are retained.
+ */
+ private MySqlHybridSplitAssigner buildReleasedLightAssigner() {
+ return buildReleasedLightAssigner(false, true);
+ }
+
+ private MySqlHybridSplitAssigner buildReleasedLightAssigner(
+ boolean scanNewlyAddedTableEnabled, boolean
releaseSnapshotMetadataEnabled) {
+ return new MySqlHybridSplitAssigner(
+ buildConfig(scanNewlyAddedTableEnabled,
releaseSnapshotMetadataEnabled),
+ 4,
+ buildReleasedHybridCheckpoint(),
+ getMySqlSplitEnumeratorContext());
+ }
+
+ private HybridPendingSplitsState buildReleasedHybridCheckpoint() {
+ List<TableId> alreadyProcessedTables = Lists.newArrayList(new
TableId(null, DB, TABLE));
+ SnapshotPendingSplitsState snapshotState =
+ new SnapshotPendingSplitsState(
+ alreadyProcessedTables,
+ new ArrayList<>(),
+ new LinkedHashMap<>(),
+ new HashMap<>(),
+ new HashMap<>(),
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ new ArrayList<>(),
+ false,
+ true,
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE,
+ true);
+ return new HybridPendingSplitsState(snapshotState, true);
+ }
+
+ private MySqlSource<SourceRecord> buildSource(boolean
releaseSnapshotMetadataEnabled) {
+ return MySqlSource.<SourceRecord>builder()
+ .hostname("localhost")
+ .port(3306)
+ .databaseList(DB)
+ .tableList(DB + "." + TABLE)
+ .username("user")
+ .password("password")
+ .serverTimeZone("UTC")
+ .deserializer(new MySqlTestUtils.ForwardDeserializeSchema())
+ .releaseSnapshotMetadataEnabled(releaseSnapshotMetadataEnabled)
+ .build();
+ }
+}
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializerTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializerTest.java
index 60abb56ff1..7193962b4e 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializerTest.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/source/assigners/state/PendingSplitsStateSerializerTest.java
@@ -30,6 +30,7 @@ import io.debezium.relational.TableEditor;
import io.debezium.relational.TableId;
import io.debezium.relational.history.TableChanges;
import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -95,7 +96,7 @@ class PendingSplitsStateSerializerTest {
@MethodSource("params")
void testRepeatedSerializationCache(PendingSplitsState state) throws
Exception {
final PendingSplitsStateSerializer serializer =
- new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE);
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, true);
final byte[] ser1 = serializer.serialize(state);
final byte[] ser2 = serializer.serialize(state);
@@ -107,7 +108,7 @@ class PendingSplitsStateSerializerTest {
@MethodSource("params")
void testOutputIsFinallyCleared(PendingSplitsState state) throws Exception
{
final PendingSplitsStateSerializer serializer =
- new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE);
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, true);
final byte[] ser1 = serializer.serialize(state);
state.serializedFormCache = null;
@@ -121,10 +122,100 @@ class PendingSplitsStateSerializerTest {
Assertions.assertThat(ser1).isEqualTo(ser2);
}
+ @Test
+ void testSerializeAndDeserializeReleasedHybridState() throws Exception {
+ // The "light" state produced after releasing the snapshot metadata
must round-trip, and
+ // the persisted released flag (PendingSplitsStateSerializer v6) must
survive.
+ HybridPendingSplitsState released =
getTestReleasedHybridPendingSplitsState();
+ PendingSplitsState roundTripped =
serializeAndDeserializeSourceEnumState(released);
+ Assertions.assertThat(roundTripped).isEqualTo(released);
+
+ SnapshotPendingSplitsState snapshot =
+ ((HybridPendingSplitsState)
roundTripped).getSnapshotPendingSplits();
+ Assertions.assertThat(snapshot.getAssignedSplits()).isEmpty();
+ Assertions.assertThat(snapshot.getSplitFinishedOffsets()).isEmpty();
+ Assertions.assertThat(snapshot.getTableSchemas()).isEmpty();
+
Assertions.assertThat(snapshot.getAlreadyProcessedTables()).isNotEmpty();
+ Assertions.assertThat(snapshot.isSnapshotMetaReleased()).isTrue();
+ Assertions.assertThat(((HybridPendingSplitsState)
roundTripped).isBinlogSplitAssigned())
+ .isTrue();
+ }
+
+ @Test
+ void testDeserializeV5SnapshotStateDefaultsReleasedFlagToFalse() throws
Exception {
+ // A v5 checkpoint written before FLINK-39775 has no released-flag
byte. For a snapshot
+ // state that byte is the last one written, so a v6 stream without its
final byte is exactly
+ // the v5 wire format. Deserializing it under the current version must
default
+ // snapshotMetaReleased to false and otherwise round-trip unchanged.
+ SnapshotPendingSplitsState state =
getTestSnapshotPendingSplitsState(false);
+ PendingSplitsStateSerializer serializer =
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, true);
+ byte[] v6 = serializer.serialize(state);
+ byte[] v5 = Arrays.copyOf(v6, v6.length - 1);
+
+ PendingSplitsState restored = serializer.deserialize(5, v5);
+
+
Assertions.assertThat(restored).isInstanceOf(SnapshotPendingSplitsState.class);
+ Assertions.assertThat(((SnapshotPendingSplitsState)
restored).isSnapshotMetaReleased())
+ .isFalse();
+ Assertions.assertThat(restored).isEqualTo(state);
+ }
+
+ @Test
+ void testReleaseDisabledJobKeepsWritingV5Format() throws Exception {
+ // A job that leaves
scan.incremental.snapshot.metadata.release.enabled off must keep
+ // writing
+ // the v5 format (no released flag), so it can still be restored by an
older connector
+ // build.
+ SnapshotPendingSplitsState state =
getTestSnapshotPendingSplitsState(false);
+ PendingSplitsStateSerializer v5Serializer =
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, false);
+ PendingSplitsStateSerializer v6Serializer =
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, true);
+
+ Assertions.assertThat(v5Serializer.getVersion()).isEqualTo(5);
+ Assertions.assertThat(v6Serializer.getVersion()).isEqualTo(6);
+
+ byte[] v6 = v6Serializer.serialize(state);
+ // Clear the lazily cached serialized form so the v5 serializer
re-serializes the state.
+ state.serializedFormCache = null;
+ byte[] v5 = v5Serializer.serialize(state);
+
+ // For a snapshot state the released flag is the final byte, so the v5
bytes are exactly the
+ // v6 bytes without that trailing byte, i.e. the format an older
connector wrote.
+ Assertions.assertThat(v5).isEqualTo(Arrays.copyOf(v6, v6.length - 1));
+
+ // A default-off job restores its own v5 checkpoint with the flag
defaulting to false.
+ PendingSplitsState restored = v5Serializer.deserialize(5, v5);
+ Assertions.assertThat(restored).isEqualTo(state);
+ Assertions.assertThat(((SnapshotPendingSplitsState)
restored).isSnapshotMetaReleased())
+ .isFalse();
+ }
+
+ @Test
+ void testReleaseDisabledSerializerRoundTripsHybridStateAtV5() throws
Exception {
+ // The option-off serializer round-trips a hybrid state at v5 (the
released flag is written
+ // for neither the snapshot part nor read back), leaving the flag
false.
+ HybridPendingSplitsState state =
getTestHybridPendingSplitsState(false);
+ PendingSplitsStateSerializer v5Serializer =
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, false);
+
+ byte[] serialized = v5Serializer.serialize(state);
+ PendingSplitsState restored =
+ v5Serializer.deserialize(v5Serializer.getVersion(),
serialized);
+
+ Assertions.assertThat(restored).isEqualTo(state);
+ Assertions.assertThat(
+ ((HybridPendingSplitsState) restored)
+ .getSnapshotPendingSplits()
+ .isSnapshotMetaReleased())
+ .isFalse();
+ }
+
static PendingSplitsState
serializeAndDeserializeSourceEnumState(PendingSplitsState state)
throws Exception {
final PendingSplitsStateSerializer serializer =
- new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE);
+ new
PendingSplitsStateSerializer(MySqlSplitSerializer.INSTANCE, true);
byte[] serialized = serializer.serialize(state);
return serializer.deserialize(serializer.getVersion(), serialized);
}
@@ -203,6 +294,28 @@ class PendingSplitsStateSerializerTest {
getTestSnapshotPendingSplitsState(checkpointWhenSplitting),
false);
}
+ private static HybridPendingSplitsState
getTestReleasedHybridPendingSplitsState() {
+ // After releasing the snapshot metadata, the hybrid state is just a
normal state
+ // with empty assigned/finished/schema maps and
isBinlogSplitAssigned=true.
+ final List<TableId> alreadyProcessedTables = new ArrayList<>();
+ alreadyProcessedTables.add(tableId0);
+ alreadyProcessedTables.add(tableId1);
+ SnapshotPendingSplitsState snapshotState =
+ new SnapshotPendingSplitsState(
+ alreadyProcessedTables,
+ new ArrayList<>(),
+ new LinkedHashMap<>(),
+ new HashMap<>(),
+ new HashMap<>(),
+ AssignerStatus.INITIAL_ASSIGNING_FINISHED,
+ new ArrayList<>(),
+ false,
+ true,
+ ChunkSplitterState.NO_SPLITTING_TABLE_STATE,
+ true);
+ return new HybridPendingSplitsState(snapshotState, true);
+ }
+
private static BinlogPendingSplitsState getTestBinlogPendingSplitsState() {
return new BinlogPendingSplitsState(true);
}
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
new file mode 100644
index 0000000000..7fb2206645
--- /dev/null
+++
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
@@ -0,0 +1,66 @@
+/*
+ * 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.mysql.source.enumerator;
+
+import org.apache.flink.api.connector.source.Boundedness;
+import
org.apache.flink.cdc.connectors.mysql.source.assigners.MySqlBinlogSplitAssigner;
+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.table.StartupOptions;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZoneId;
+
+import static
org.apache.flink.cdc.connectors.mysql.testutils.MetricsUtils.getMySqlSplitEnumeratorContext;
+
+/** Tests for {@link MySqlSourceEnumerator}. */
+class MySqlSourceEnumeratorTest {
+
+ @Test
+ void testBinlogSplitMetaAssembledEventIsNoOpForBinlogOnlyAssigner() {
+ // For binlog-only startup modes the assigner is a
MySqlBinlogSplitAssigner, not the hybrid
+ // one. The reader still sends BinlogSplitMetaAssembledEvent, and the
enumerator must treat
+ // it as a safe no-op rather than failing.
+ MySqlSourceConfig config =
+ new MySqlSourceConfigFactory()
+ .startupOptions(StartupOptions.latest())
+ .databaseList("db")
+ .tableList("db.t")
+ .hostname("localhost")
+ .port(3306)
+ .username("root")
+ .password("")
+ .serverTimeZone(ZoneId.of("UTC").toString())
+ .createConfig(0);
+ MySqlSourceEnumerator enumerator =
+ new MySqlSourceEnumerator(
+ getMySqlSplitEnumeratorContext(),
+ config,
+ new MySqlBinlogSplitAssigner(config),
+ Boundedness.CONTINUOUS_UNBOUNDED);
+
+ Assertions.assertThatCode(
+ () ->
+ enumerator.handleSourceEvent(
+ 0, new
BinlogSplitMetaAssembledEvent(0L)))
+ .doesNotThrowAnyException();
+ }
+}
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 f2fc642669..137df37c57 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
@@ -28,9 +28,12 @@ import
org.apache.flink.cdc.connectors.mysql.source.assigners.MySqlBinlogSplitAs
import
org.apache.flink.cdc.connectors.mysql.source.assigners.MySqlSnapshotSplitAssigner;
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.BinlogSplitMetaEvent;
import
org.apache.flink.cdc.connectors.mysql.source.events.FinishedSnapshotSplitsAckEvent;
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;
import org.apache.flink.cdc.connectors.mysql.source.split.MySqlBinlogSplit;
import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSnapshotSplit;
import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSplit;
@@ -379,6 +382,106 @@ class MySqlSourceReaderTest extends MySqlSourceTestBase {
restartReader.close();
}
+ @Test
+ void testReaderSendsBinlogSplitMetaAssembledEventForCompleteSplit() throws
Exception {
+ // When a complete binlog split enters reading, the reader must notify
the
+ // enumerator that its finished snapshot split metadata is fully
assembled, so the
+ // coordinator can release the retained snapshot metadata.
+ customerDatabase.createAndInitialize();
+ final MySqlSourceConfig sourceConfig = getConfig(new String[]
{"customers"});
+ MySqlSplit binlogSplit;
+ try (MySqlConnection jdbc =
DebeziumUtils.createMySqlConnection(sourceConfig)) {
+ Map<TableId, TableChanges.TableChange> tableSchemas =
+ TableDiscoveryUtils.discoverSchemaForCapturedTables(
+ new MySqlPartition(
+
sourceConfig.getMySqlConnectorConfig().getLogicalName()),
+ sourceConfig,
+ jdbc);
+ binlogSplit =
+ MySqlBinlogSplit.fillTableSchemas(
+ createBinlogSplit(sourceConfig).asBinlogSplit(),
tableSchemas);
+ }
+ // a binlog split created from the latest startup offset carries no
finished snapshot split
+ // info, so it is already complete on receipt
+
Assertions.assertThat(binlogSplit.asBinlogSplit().isCompletedSplit()).isTrue();
+
+ TestingReaderContext readerContext = new TestingReaderContext();
+ MySqlSourceReader<SourceRecord> reader = createReader(sourceConfig,
readerContext, 1);
+ reader.start();
+ reader.addSplits(Collections.singletonList(binlogSplit));
+
+ // the split was complete inline (no meta groups requested), so the
reader reports the
+ // COMPLETE_WITHOUT_META_GENERATION sentinel rather than a served
generation
+ Assertions.assertThat(readerContext.getSentEvents())
+ .filteredOn(event -> event instanceof
BinlogSplitMetaAssembledEvent)
+ .extracting(
+ event ->
+ ((BinlogSplitMetaAssembledEvent) event)
+ .getBinlogAssignmentGeneration())
+
.containsExactly(BinlogSplitMetaAssembledEvent.COMPLETE_WITHOUT_META_GENERATION);
+ reader.close();
+ }
+
+ @Test
+ void testReaderEchoesBinlogAssignmentGenerationInAssembledEvent() throws
Exception {
+ // A binlog split assembled from divided meta groups must echo the
binlog assignment
+ // generation it was served under, so the coordinator can reject a
stale assembled event
+ // from a failed attempt. Drive the divided-meta path: give the reader
an incomplete binlog
+ // split, answer its meta request with a group stamped with a
generation, and assert the
+ // assembled event the reader sends back carries that same generation.
+ customerDatabase.createAndInitialize();
+ final MySqlSourceConfig sourceConfig = getConfig(new String[]
{"customers"});
+ final TableId tableId =
TableId.parse(customerDatabase.getDatabaseName() + ".customers");
+
+ // one finished split info is still outstanding, so the split is
incomplete on receipt
+ MySqlBinlogSplit incompleteSplit =
+ new MySqlBinlogSplit(
+ "binlog-split",
+ BinlogOffset.ofEarliest(),
+ null,
+ new ArrayList<>(),
+ new HashMap<>(),
+ 1,
+ false);
+ Assertions.assertThat(incompleteSplit.isCompletedSplit()).isFalse();
+
+ TestingReaderContext readerContext = new TestingReaderContext();
+ MySqlSourceReader<SourceRecord> reader = createReader(sourceConfig,
readerContext, 0);
+ reader.start();
+ reader.addSplits(Collections.singletonList(incompleteSplit));
+
+ // the coordinator answers the reader's meta request with the
outstanding group, stamped
+ // with the current binlog assignment generation
+ long generation = 7L;
+ FinishedSnapshotSplitInfo finishedInfo =
+ new FinishedSnapshotSplitInfo(
+ tableId,
+ tableId + ":0",
+ new Object[] {0},
+ new Object[] {100},
+ BinlogOffset.ofBinlogFilePosition("mysql-bin.000001",
4L));
+ reader.handleSourceEvents(
+ new BinlogSplitMetaEvent(
+ incompleteSplit.splitId(),
+ 0,
+ Collections.singletonList(
+
FinishedSnapshotSplitInfo.serialize(finishedInfo)),
+ 1,
+ generation));
+
+ BinlogSplitMetaAssembledEvent assembled =
+ readerContext.getSentEvents().stream()
+ .filter(event -> event instanceof
BinlogSplitMetaAssembledEvent)
+ .map(event -> (BinlogSplitMetaAssembledEvent) event)
+ .reduce((first, second) -> second)
+ .orElseThrow(
+ () ->
+ new AssertionError(
+ "reader did not send a
BinlogSplitMetaAssembledEvent"));
+
Assertions.assertThat(assembled.getBinlogAssignmentGeneration()).isEqualTo(generation);
+ reader.close();
+ }
+
@Test
void testRemoveSplitAccordingToNewFilter() throws Exception {
inventoryDatabase.createAndInitialize();
diff --git
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactoryTest.java
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactoryTest.java
index 01c2dff84d..33abc21ddb 100644
---
a/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactoryTest.java
+++
b/flink-cdc-connect/flink-cdc-source-connectors/flink-connector-mysql-cdc/src/test/java/org/apache/flink/cdc/connectors/mysql/table/MySqlTableSourceFactoryTest.java
@@ -122,6 +122,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.initial(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -172,6 +173,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.initial(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
"testCol",
@@ -218,6 +220,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.initial(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -262,6 +265,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.latest(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -322,6 +326,7 @@ class MySqlTableSourceFactoryTest {
CHUNK_KEY_EVEN_DISTRIBUTION_FACTOR_LOWER_BOUND.defaultValue(),
StartupOptions.initial(),
true,
+ false,
true,
jdbcProperties,
Duration.ofMillis(15213),
@@ -382,6 +387,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.specificOffset(offsetFile, offsetPos),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -424,6 +430,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.initial(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -467,6 +474,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.earliest(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -511,6 +519,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.timestamp(0L),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -553,6 +562,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.latest(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -600,6 +610,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.initial(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -803,6 +814,7 @@ class MySqlTableSourceFactoryTest {
StartupOptions.initial(),
false,
false,
+ false,
new Properties(),
HEARTBEAT_INTERVAL.defaultValue(),
null,
@@ -814,6 +826,19 @@ class MySqlTableSourceFactoryTest {
Assertions.assertThat(actualSource).isEqualTo(expectedSource);
}
+ @Test
+ void testReleaseSnapshotMetadataOption() {
+ // Setting the option must be accepted (it is registered in
optionalOptions, so there is no
+ // "unsupported options" validation error) and must change the
produced source.
+ Map<String, String> withRelease = getAllOptions();
+ withRelease.put("scan.incremental.snapshot.metadata.release.enabled",
"true");
+ DynamicTableSource sourceWithRelease = createTableSource(withRelease);
+
+ Assertions.assertThat(sourceWithRelease)
+ .isInstanceOf(MySqlTableSource.class)
+ .isNotEqualTo(createTableSource(getAllOptions()));
+ }
+
private Map<String, String> getAllOptions() {
Map<String, String> options = new HashMap<>();
options.put("connector", "mysql-cdc");