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

pvary pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new 0c3465b541 Flink: Fix DynamicSink silently dropping commits after 
stateless restart (#18303)
0c3465b541 is described below

commit 0c3465b5416e7929493efe1ba719e583df9e6956
Author: GuoYu <[email protected]>
AuthorDate: Thu Nov 5 01:33:07 2026 +0800

    Flink: Fix DynamicSink silently dropping commits after stateless restart 
(#18303)
---
 docs/docs/flink-writes.md                          |   1 +
 .../flink/sink/dynamic/DynamicCommittable.java     |  24 ++-
 .../sink/dynamic/DynamicCommittableSerializer.java |  24 ++-
 .../flink/sink/dynamic/DynamicCommitter.java       |  77 ++++---
 .../flink/sink/dynamic/DynamicIcebergSink.java     |   4 +-
 .../sink/dynamic/DynamicWriteResultAggregator.java |   7 +-
 .../iceberg/flink/sink/dynamic/JobOperatorKey.java |  12 +-
 .../dynamic/TestDynamicCommittableSerializer.java  |  39 +++-
 .../flink/sink/dynamic/TestDynamicCommitter.java   | 233 ++++++++++++++++++---
 .../flink/sink/dynamic/TestDynamicIcebergSink.java |  33 +++
 .../dynamic/TestDynamicWriteResultAggregator.java  |   8 +-
 11 files changed, 388 insertions(+), 74 deletions(-)

diff --git a/docs/docs/flink-writes.md b/docs/docs/flink-writes.md
index e01736d69c..6e1a4ec095 100644
--- a/docs/docs/flink-writes.md
+++ b/docs/docs/flink-writes.md
@@ -607,3 +607,4 @@ Forward and regular records can be mixed in the same 
pipeline. The processor rou
 - **Range distribution mode**: Currently, the dynamic sink does not support 
the `RANGE` distribution mode, if set, it will fall back to `HASH`.
 - **Property Precedence Note**: When conflicts occur between table properties 
and sink properties, the sink properties will override the table properties 
configuration.
 - **Table Format Version upgrade**: Dynamic sink does not support upgrading a 
table with dynamic records. The job should not be running while the V2 to V3 
upgrade is in progress.
+- **Snapshot summary**: Commits record `flink.sink-id` in the snapshot 
summary. It identifies the sink run that wrote the snapshot and is used to skip 
already committed checkpoints. Snapshots from older releases without it are 
identified by `flink.job-id`.
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittable.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittable.java
index 4f0b68573f..1dec44e195 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittable.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittable.java
@@ -26,8 +26,10 @@ import 
org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
 
 /**
  * The aggregated results of a single checkpoint which should be committed. 
Containing the
- * serialized {@link DeltaManifests} files - which contains the commit data, 
and the jobId,
- * operatorId, checkpointId triplet to identify the specific commit.
+ * serialized {@link DeltaManifests} files - which contains the commit data, 
and the jobId, sinkId,
+ * operatorId, checkpointId to identify the specific commit.
+ *
+ * <p>Committables restored from older state use the jobId as sinkId.
  *
  * <p>{@link DynamicCommittableSerializer} is used to serialize {@link 
DynamicCommittable} between
  * the {@link DynamicWriter} and the {@link DynamicWriteResultAggregator}.
@@ -37,14 +39,21 @@ class DynamicCommittable implements Serializable {
   private final TableKey key;
   private final byte[][] manifests;
   private final String jobId;
+  private final String sinkId;
   private final String operatorId;
   private final long checkpointId;
 
   DynamicCommittable(
-      TableKey key, byte[][] manifests, String jobId, String operatorId, long 
checkpointId) {
+      TableKey key,
+      byte[][] manifests,
+      String jobId,
+      String sinkId,
+      String operatorId,
+      long checkpointId) {
     this.key = key;
     this.manifests = manifests;
     this.jobId = jobId;
+    this.sinkId = sinkId;
     this.operatorId = operatorId;
     this.checkpointId = checkpointId;
   }
@@ -61,6 +70,10 @@ class DynamicCommittable implements Serializable {
     return jobId;
   }
 
+  String sinkId() {
+    return sinkId;
+  }
+
   String operatorId() {
     return operatorId;
   }
@@ -80,12 +93,14 @@ class DynamicCommittable implements Serializable {
         && Objects.equals(key, that.key)
         && Arrays.deepEquals(manifests, that.manifests)
         && Objects.equals(jobId, that.jobId)
+        && Objects.equals(sinkId, that.sinkId)
         && Objects.equals(operatorId, that.operatorId);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(key, Arrays.deepHashCode(manifests), jobId, 
operatorId, checkpointId);
+    return Objects.hash(
+        key, Arrays.deepHashCode(manifests), jobId, sinkId, operatorId, 
checkpointId);
   }
 
   @Override
@@ -93,6 +108,7 @@ class DynamicCommittable implements Serializable {
     return MoreObjects.toStringHelper(this)
         .add("key", key)
         .add("jobId", jobId)
+        .add("sinkId", sinkId)
         .add("checkpointId", checkpointId)
         .add("operatorId", operatorId)
         .toString();
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittableSerializer.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittableSerializer.java
index d599d29dba..e4f3808cd8 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittableSerializer.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommittableSerializer.java
@@ -33,10 +33,11 @@ class DynamicCommittableSerializer implements 
SimpleVersionedSerializer<DynamicC
 
   private static final int VERSION_1 = 1;
   private static final int VERSION_2 = 2;
+  private static final int VERSION_3 = 3;
 
   @Override
   public int getVersion() {
-    return VERSION_2;
+    return VERSION_3;
   }
 
   @Override
@@ -45,6 +46,7 @@ class DynamicCommittableSerializer implements 
SimpleVersionedSerializer<DynamicC
     DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
     committable.key().serializeTo(view);
     view.writeUTF(committable.jobId());
+    view.writeUTF(committable.sinkId());
     view.writeUTF(committable.operatorId());
     view.writeLong(committable.checkpointId());
 
@@ -65,6 +67,8 @@ class DynamicCommittableSerializer implements 
SimpleVersionedSerializer<DynamicC
       return deserializeV1(serialized);
     } else if (version == VERSION_2) {
       return deserializeV2(serialized);
+    } else if (version == VERSION_3) {
+      return deserializeV3(serialized);
     }
 
     throw new IOException("Unrecognized version or corrupt state: " + version);
@@ -83,6 +87,7 @@ class DynamicCommittableSerializer implements 
SimpleVersionedSerializer<DynamicC
         new TableKey(key.tableName(), key.branch()),
         new byte[][] {manifestBuf},
         jobId,
+        jobId,
         operatorId,
         checkpointId);
   }
@@ -93,7 +98,22 @@ class DynamicCommittableSerializer implements 
SimpleVersionedSerializer<DynamicC
     String jobId = view.readUTF();
     String operatorId = view.readUTF();
     long checkpointId = view.readLong();
+    byte[][] manifests = readManifests(view);
+    return new DynamicCommittable(key, manifests, jobId, jobId, operatorId, 
checkpointId);
+  }
+
+  private DynamicCommittable deserializeV3(byte[] serialized) throws 
IOException {
+    DataInputDeserializer view = new DataInputDeserializer(serialized);
+    TableKey key = TableKey.deserializeFrom(view);
+    String jobId = view.readUTF();
+    String sinkId = view.readUTF();
+    String operatorId = view.readUTF();
+    long checkpointId = view.readLong();
+    byte[][] manifests = readManifests(view);
+    return new DynamicCommittable(key, manifests, jobId, sinkId, operatorId, 
checkpointId);
+  }
 
+  private static byte[][] readManifests(DataInputDeserializer view) throws 
IOException {
     byte[][] manifestsBuf = new byte[view.readInt()][];
     for (int i = 0; i < manifestsBuf.length; i++) {
       byte[] manifest = new byte[view.readInt()];
@@ -101,6 +121,6 @@ class DynamicCommittableSerializer implements 
SimpleVersionedSerializer<DynamicC
       manifestsBuf[i] = manifest;
     }
 
-    return new DynamicCommittable(key, manifestsBuf, jobId, operatorId, 
checkpointId);
+    return manifestsBuf;
   }
 }
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommitter.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommitter.java
index c8b9a097ad..7a4bdf86aa 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommitter.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicCommitter.java
@@ -70,7 +70,7 @@ import org.slf4j.LoggerFactory;
  *   <li>There is no late checkpoint - if checkpoint 'x' has received in one 
call, then after a
  *       successful run only checkpoints &gt; x will arrive
  *   <li>There is no other writer which would generate another commit to the 
same branch with the
- *       same jobId-operatorId-checkpointId triplet
+ *       same sinkId-operatorId-checkpointId triplet
  * </ul>
  */
 @Internal
@@ -82,6 +82,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
   private static final long INITIAL_CHECKPOINT_ID = -1L;
 
   private static final String FLINK_JOB_ID = "flink.job-id";
+  private static final String SINK_ID = "flink.sink-id";
   private static final String OPERATOR_ID = "flink.operator-id";
   private final Map<String, String> snapshotProperties;
   private final boolean replacePartitions;
@@ -118,9 +119,8 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
               |              |                  |              |
               |              |                  |              +-- commit 
requests at that checkpoint
               |              |                  +-- checkpointId, sorted 
ascending so older commits go first
-              |              +-- (jobId, operatorId) of the producing 
aggregator; deduplication against the
-              |                  table's snapshot summaries is per (jobId, 
operatorId), so each group is
-              |                  walked against the ancestor chain 
independently
+              |              +-- (sinkId, operatorId) of the producing 
aggregator; each group is
+              |                  deduplicated against the ancestor chain 
independently
               +-- (tableName, branch); we load the table and its ancestor 
chain once per TableKey
 
       Each (table, branch, checkpoint) triplet must have only one commit 
request.
@@ -156,17 +156,17 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
 
       List<Map.Entry<JobOperatorKey, NavigableMap<Long, 
List<CommitRequest<DynamicCommittable>>>>>
           jobEntries = Lists.newArrayList(tableEntry.getValue().entrySet());
-      // Preserve checkpoint order across groups so that older-jobId commits 
land before newer-jobId
-      // ones when the batch mixes committables from different jobIds (e.g. 
state replay after a
-      // restart). Within a (jobId, operatorId) group, checkpoint order is 
already guaranteed by
-      // the inner NavigableMap.
+      // Preserve checkpoint order across groups so that older-sinkId commits 
land before
+      // newer-sinkId ones when the batch mixes committables from different 
runs (e.g. state replay
+      // after a restart). Within a (sinkId, operatorId) group, checkpoint 
order is already
+      // guaranteed by the inner NavigableMap.
       jobEntries.sort(Comparator.comparingLong(entry -> 
entry.getValue().firstKey()));
 
       for (Map.Entry<JobOperatorKey, NavigableMap<Long, 
List<CommitRequest<DynamicCommittable>>>>
           jobEntry : jobEntries) {
         JobOperatorKey jobKey = jobEntry.getKey();
         long maxCommittedCheckpointId =
-            getMaxCommittedCheckpointId(ancestors, jobKey.jobId(), 
jobKey.operatorId());
+            getMaxCommittedCheckpointId(ancestors, jobKey.sinkId(), 
jobKey.operatorId());
 
         NavigableMap<Long, List<CommitRequest<DynamicCommittable>>> 
skippedCommitRequests =
             jobEntry.getValue().headMap(maxCommittedCheckpointId, true);
@@ -181,22 +181,24 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
             jobEntry.getValue().tailMap(maxCommittedCheckpointId, false);
         if (!uncommitted.isEmpty()) {
           commitPendingRequests(
-              table, tableKey.branch(), uncommitted, jobKey.jobId(), 
jobKey.operatorId());
+              table,
+              tableKey.branch(),
+              uncommitted,
+              jobKey.jobId(),
+              jobKey.sinkId(),
+              jobKey.operatorId());
         }
       }
     }
   }
 
   private static long getMaxCommittedCheckpointId(
-      Iterable<Snapshot> ancestors, String flinkJobId, String operatorId) {
+      Iterable<Snapshot> ancestors, String sinkId, String operatorId) {
     long lastCommittedCheckpointId = INITIAL_CHECKPOINT_ID;
 
     for (Snapshot ancestor : ancestors) {
       Map<String, String> summary = ancestor.summary();
-      String snapshotFlinkJobId = summary.get(FLINK_JOB_ID);
-      String snapshotOperatorId = summary.get(OPERATOR_ID);
-      if (flinkJobId.equals(snapshotFlinkJobId)
-          && (snapshotOperatorId == null || 
snapshotOperatorId.equals(operatorId))) {
+      if (writtenBySameSink(summary, sinkId, operatorId)) {
         String value = summary.get(MAX_COMMITTED_CHECKPOINT_ID);
         if (value != null) {
           lastCommittedCheckpointId = Long.parseLong(value);
@@ -208,6 +210,18 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
     return lastCommittedCheckpointId;
   }
 
+  private static boolean writtenBySameSink(
+      Map<String, String> summary, String sinkId, String operatorId) {
+    String snapshotOperatorId = summary.get(OPERATOR_ID);
+    if (snapshotOperatorId != null && !snapshotOperatorId.equals(operatorId)) {
+      return false;
+    }
+
+    // Snapshots written before the sinkId was introduced are identified by 
the jobId
+    String snapshotSinkId = summary.get(SINK_ID);
+    return sinkId.equals(snapshotSinkId != null ? snapshotSinkId : 
summary.get(FLINK_JOB_ID));
+  }
+
   /**
    * Commits the data to the Iceberg table by reading the file data from the 
{@link DeltaManifests}
    * ordered by the checkpointId, and writing the new snapshot to the Iceberg 
table. The {@link
@@ -216,6 +230,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
    *
    * @param commitRequestMap The checkpointId to {@link CommitRequest} map of 
the changes to commit
    * @param newFlinkJobId The jobId to store in the {@link SnapshotSummary}
+   * @param sinkId The sinkId to store in the {@link SnapshotSummary}
    * @param operatorId The operatorId to store in the {@link SnapshotSummary}
    * @throws IOException On commit failure
    */
@@ -224,6 +239,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
       String branch,
       NavigableMap<Long, List<CommitRequest<DynamicCommittable>>> 
commitRequestMap,
       String newFlinkJobId,
+      String sinkId,
       String operatorId)
       throws IOException {
     long checkpointId = commitRequestMap.lastKey();
@@ -258,9 +274,9 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
     }
 
     if (replacePartitions) {
-      replacePartitions(table, branch, pendingResults, newFlinkJobId, 
operatorId);
+      replacePartitions(table, branch, pendingResults, newFlinkJobId, sinkId, 
operatorId);
     } else {
-      commitDeltaTxn(table, branch, pendingResults, newFlinkJobId, operatorId);
+      commitDeltaTxn(table, branch, pendingResults, newFlinkJobId, sinkId, 
operatorId);
     }
 
     FlinkManifestUtil.deleteCommittedManifests(table, manifests, 
newFlinkJobId, checkpointId);
@@ -271,6 +287,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
       String branch,
       NavigableMap<Long, List<WriteResult>> pendingResults,
       String newFlinkJobId,
+      String sinkId,
       String operatorId) {
     // Iceberg tables are unsorted. So the order of the append data does not 
matter.
     // Hence, we commit everything in one snapshot.
@@ -292,6 +309,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
         summary,
         "dynamic partition overwrite",
         newFlinkJobId,
+        sinkId,
         operatorId,
         pendingResults.lastKey());
   }
@@ -301,6 +319,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
       String branch,
       NavigableMap<Long, List<WriteResult>> pendingResults,
       String newFlinkJobId,
+      String sinkId,
       String operatorId) {
     for (Map.Entry<Long, List<WriteResult>> e : pendingResults.entrySet()) {
       long checkpointId = e.getKey();
@@ -328,7 +347,15 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
       // pending checkpoints here are very rare to occur, i.e. only with very 
short checkpoint
       // intervals or when concurrent checkpointing is enabled.
       commitOperation(
-          table, branch, rowDelta, summary, "rowDelta", newFlinkJobId, 
operatorId, checkpointId);
+          table,
+          branch,
+          rowDelta,
+          summary,
+          "rowDelta",
+          newFlinkJobId,
+          sinkId,
+          operatorId,
+          checkpointId);
     }
   }
 
@@ -340,20 +367,20 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
 
   private static class MaxCommittedCheckpointIdValidator implements 
SnapshotAncestryValidator {
     private final long stagedCheckpointId;
-    private final String flinkJobId;
+    private final String sinkId;
     private final String flinkOperatorId;
 
     private MaxCommittedCheckpointIdValidator(
-        long stagedCheckpointId, String flinkJobId, String flinkOperatorId) {
+        long stagedCheckpointId, String sinkId, String flinkOperatorId) {
       this.stagedCheckpointId = stagedCheckpointId;
-      this.flinkJobId = flinkJobId;
+      this.sinkId = sinkId;
       this.flinkOperatorId = flinkOperatorId;
     }
 
     @Override
     public boolean validate(Iterable<Snapshot> baseSnapshots) {
       long maxCommittedCheckpointId =
-          getMaxCommittedCheckpointId(baseSnapshots, flinkJobId, 
flinkOperatorId);
+          getMaxCommittedCheckpointId(baseSnapshots, sinkId, flinkOperatorId);
       if (maxCommittedCheckpointId >= stagedCheckpointId) {
         throw new MaxCommittedCheckpointMismatchException();
       }
@@ -370,6 +397,7 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
       CommitSummary summary,
       String description,
       String newFlinkJobId,
+      String sinkId,
       String operatorId,
       long checkpointId) {
 
@@ -385,10 +413,11 @@ class DynamicCommitter implements 
Committer<DynamicCommittable> {
     // used by the sink.
     operation.set(MAX_COMMITTED_CHECKPOINT_ID, Long.toString(checkpointId));
     operation.set(FLINK_JOB_ID, newFlinkJobId);
+    operation.set(SINK_ID, sinkId);
+
     operation.set(OPERATOR_ID, operatorId);
     operation.toBranch(branch);
-    operation.validateWith(
-        new MaxCommittedCheckpointIdValidator(checkpointId, newFlinkJobId, 
operatorId));
+    operation.validateWith(new MaxCommittedCheckpointIdValidator(checkpointId, 
sinkId, operatorId));
 
     long startNano = System.nanoTime();
     try {
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
index 3d478ecd6f..65a904bc77 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicIcebergSink.java
@@ -99,7 +99,7 @@ public class DynamicIcebergSink
     this.cacheMaximumSize = cacheMaximumSize;
     // We generate a random UUID every time when a sink is created.
     // This is used to separate files generated by different sinks writing the 
same table.
-    // Also used to generate the aggregator operator name
+    // Also used to generate the aggregator operator name, and to deduplicate 
commits
     this.sinkId = UUID.randomUUID().toString();
     this.forwardWriteResults = forwardWriteResults;
   }
@@ -168,7 +168,7 @@ public class DynamicIcebergSink
         .transform(
             prefixIfNotNull(uidPrefix, sinkId + " Pre Commit"),
             typeInformation,
-            new DynamicWriteResultAggregator(catalogLoader, cacheMaximumSize))
+            new DynamicWriteResultAggregator(catalogLoader, cacheMaximumSize, 
sinkId))
         .uid(prefixIfNotNull(uidPrefix, "-pre-commit-topology"));
   }
 
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicWriteResultAggregator.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicWriteResultAggregator.java
index 41f01c6475..7bdbf5b9b1 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicWriteResultAggregator.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/DynamicWriteResultAggregator.java
@@ -52,7 +52,7 @@ import org.slf4j.LoggerFactory;
 
 /**
  * Operator which aggregates the individual {@link WriteResult} objects to a 
single {@link
- * DynamicCommittable} per checkpoint (storing the serialized {@link 
DeltaManifests}, jobId,
+ * DynamicCommittable} per checkpoint (storing the serialized {@link 
DeltaManifests}, jobId, sinkId,
  * operatorId, checkpointId)
  */
 class DynamicWriteResultAggregator
@@ -64,6 +64,7 @@ class DynamicWriteResultAggregator
 
   private final CatalogLoader catalogLoader;
   private final int cacheMaximumSize;
+  private final String sinkId;
 
   private long lastCheckpointId = CheckpointIDCounter.INITIAL_CHECKPOINT_ID - 
1;
 
@@ -77,9 +78,10 @@ class DynamicWriteResultAggregator
   private transient int attemptId;
   private transient Catalog catalog;
 
-  DynamicWriteResultAggregator(CatalogLoader catalogLoader, int 
cacheMaximumSize) {
+  DynamicWriteResultAggregator(CatalogLoader catalogLoader, int 
cacheMaximumSize, String sinkId) {
     this.catalogLoader = catalogLoader;
     this.cacheMaximumSize = cacheMaximumSize;
+    this.sinkId = sinkId;
   }
 
   @Override
@@ -129,6 +131,7 @@ class DynamicWriteResultAggregator
                   entries.getKey(),
                   writeToManifests(entries.getKey().tableName(), 
entries.getValue(), checkpointId),
                   getContainingTask().getEnvironment().getJobID().toString(),
+                  sinkId,
                   getRuntimeContext().getOperatorUniqueID(),
                   checkpointId),
               checkpointId,
diff --git 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/JobOperatorKey.java
 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/JobOperatorKey.java
index 463c09475d..097f31b7ba 100644
--- 
a/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/JobOperatorKey.java
+++ 
b/flink/v2.3/flink/src/main/java/org/apache/iceberg/flink/sink/dynamic/JobOperatorKey.java
@@ -22,10 +22,12 @@ import java.util.Objects;
 
 class JobOperatorKey {
   private final String jobId;
+  private final String sinkId;
   private final String operatorId;
 
   JobOperatorKey(DynamicCommittable committable) {
     this.jobId = committable.jobId();
+    this.sinkId = committable.sinkId();
     this.operatorId = committable.operatorId();
   }
 
@@ -33,6 +35,10 @@ class JobOperatorKey {
     return jobId;
   }
 
+  String sinkId() {
+    return sinkId;
+  }
+
   String operatorId() {
     return operatorId;
   }
@@ -47,11 +53,13 @@ class JobOperatorKey {
       return false;
     }
 
-    return Objects.equals(jobId, that.jobId) && Objects.equals(operatorId, 
that.operatorId);
+    return Objects.equals(jobId, that.jobId)
+        && Objects.equals(sinkId, that.sinkId)
+        && Objects.equals(operatorId, that.operatorId);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(jobId, operatorId);
+    return Objects.hash(jobId, sinkId, operatorId);
   }
 }
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommittableSerializer.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommittableSerializer.java
index 16890d1f63..55da85b2e1 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommittableSerializer.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommittableSerializer.java
@@ -24,6 +24,7 @@ import static 
org.assertj.core.api.Assertions.assertThatThrownBy;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.Set;
+import java.util.UUID;
 import org.apache.flink.api.common.JobID;
 import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
 import org.apache.flink.runtime.jobgraph.OperatorID;
@@ -36,22 +37,42 @@ class TestDynamicCommittableSerializer {
           new TableKey("table", "branch"),
           new byte[][] {{3, 4}, {5, 6}},
           JobID.generate().toHexString(),
+          UUID.randomUUID().toString(),
           new OperatorID().toHexString(),
           5);
 
   @Test
   void testV1() throws IOException {
+    String jobId = JobID.generate().toHexString();
+    // Older formats have no sinkId, the jobId is used instead
     var committable =
         new DynamicCommittable(
             new TableKey("table", "branch"),
             new byte[][] {{3, 4}},
-            JobID.generate().toHexString(),
+            jobId,
+            jobId,
             new OperatorID().toHexString(),
             5);
     DynamicCommittableSerializer serializer = new 
DynamicCommittableSerializer();
     assertThat(serializer.deserialize(1, 
serializeV1(committable))).isEqualTo(committable);
   }
 
+  @Test
+  void testV2() throws IOException {
+    String jobId = JobID.generate().toHexString();
+    // Older formats have no sinkId, the jobId is used instead
+    var committable =
+        new DynamicCommittable(
+            new TableKey("table", "branch"),
+            new byte[][] {{3, 4}, {5, 6}},
+            jobId,
+            jobId,
+            new OperatorID().toHexString(),
+            5);
+    DynamicCommittableSerializer serializer = new 
DynamicCommittableSerializer();
+    assertThat(serializer.deserialize(2, 
serializeV2(committable))).isEqualTo(committable);
+  }
+
   @Test
   void testLatestVersion() throws IOException {
     DynamicCommittableSerializer serializer = new 
DynamicCommittableSerializer();
@@ -67,6 +88,22 @@ class TestDynamicCommittableSerializer {
         .isInstanceOf(IOException.class);
   }
 
+  byte[] serializeV2(DynamicCommittable committable) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
+    committable.key().serializeTo(view);
+    view.writeUTF(committable.jobId());
+    view.writeUTF(committable.operatorId());
+    view.writeLong(committable.checkpointId());
+    view.writeInt(committable.manifests().length);
+    for (byte[] manifest : committable.manifests()) {
+      view.writeInt(manifest.length);
+      view.write(manifest);
+    }
+
+    return out.toByteArray();
+  }
+
   byte[] serializeV1(DynamicCommittable committable) throws IOException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     DataOutputViewStreamWrapper view = new DataOutputViewStreamWrapper(out);
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommitter.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommitter.java
index aecad67c1a..0133879ff1 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommitter.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicCommitter.java
@@ -27,6 +27,7 @@ import java.io.Serializable;
 import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Map;
+import java.util.UUID;
 import org.apache.flink.api.common.JobID;
 import org.apache.flink.api.connector.sink2.Committer.CommitRequest;
 import org.apache.flink.api.connector.sink2.mocks.MockCommitRequest;
@@ -67,6 +68,7 @@ import org.junit.jupiter.params.provider.ValueSource;
 class TestDynamicCommitter {
 
   static final String DB = "db";
+  static final String SINK_ID = "sinkId";
   static final String TABLE1 = "table";
   static final String TABLE2 = "table2";
 
@@ -170,7 +172,8 @@ class TestDynamicCommitter {
     TableKey tableKey3 = new TableKey(TABLE2, "branch2");
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -188,15 +191,18 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest1 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey1, deltaManifests1, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey1, deltaManifests1, jobId, SINK_ID, operatorId, 
checkpointId));
 
     CommitRequest<DynamicCommittable> commitRequest2 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey2, deltaManifests2, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey2, deltaManifests2, jobId, SINK_ID, operatorId, 
checkpointId));
 
     CommitRequest<DynamicCommittable> commitRequest3 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey3, deltaManifests3, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey3, deltaManifests3, jobId, SINK_ID, operatorId, 
checkpointId));
 
     dynamicCommitter.commit(Sets.newHashSet(commitRequest1, commitRequest2, 
commitRequest3));
 
@@ -210,6 +216,7 @@ class TestDynamicCommitter {
                 .put("added-records", "42")
                 .put("changed-partition-count", "1")
                 .put("flink.job-id", jobId)
+                .put("flink.sink-id", SINK_ID)
                 .put("flink.max-committed-checkpoint-id", "" + checkpointId)
                 .put("flink.operator-id", operatorId)
                 .put("total-data-files", "1")
@@ -227,6 +234,7 @@ class TestDynamicCommitter {
                 .put("added-records", "42")
                 .put("changed-partition-count", "1")
                 .put("flink.job-id", jobId)
+                .put("flink.sink-id", SINK_ID)
                 .put("flink.max-committed-checkpoint-id", "" + checkpointId)
                 .put("flink.operator-id", operatorId)
                 .put("total-data-files", "1")
@@ -247,6 +255,7 @@ class TestDynamicCommitter {
                 .put("added-records", "42")
                 .put("changed-partition-count", "1")
                 .put("flink.job-id", jobId)
+                .put("flink.sink-id", SINK_ID)
                 .put("flink.max-committed-checkpoint-id", "" + checkpointId)
                 .put("flink.operator-id", operatorId)
                 .put("total-data-files", "1")
@@ -280,7 +289,8 @@ class TestDynamicCommitter {
     TableKey tableKey = new TableKey(TABLE1, "branch");
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -294,13 +304,15 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey, deltaManifests, jobId, jobId, operatorId, 
checkpointId));
 
     dynamicCommitter.commit(Sets.newHashSet(commitRequest));
 
     CommitRequest<DynamicCommittable> oldCommitRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests, jobId, 
operatorId, checkpointId - 1));
+            new DynamicCommittable(
+                tableKey, deltaManifests, jobId, jobId, operatorId, 
checkpointId - 1));
 
     // Old commits requests shouldn't affect the result
     dynamicCommitter.commit(Sets.newHashSet(oldCommitRequest));
@@ -347,7 +359,8 @@ class TestDynamicCommitter {
             previousCommitterMetrics);
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -356,6 +369,7 @@ class TestDynamicCommitter {
     // Operator id is stable across Flink job restarts, jobId is not.
     final String operatorId = new OperatorID().toHexString();
     final String previousJobIdStr = previousJobId.toHexString();
+    final String previousSinkId = UUID.randomUUID().toString();
     final int previousCheckpointId = 10;
 
     byte[][] previousManifests =
@@ -364,7 +378,12 @@ class TestDynamicCommitter {
 
     DynamicCommittable previousCommittable =
         new DynamicCommittable(
-            tableKey, previousManifests, previousJobIdStr, operatorId, 
previousCheckpointId);
+            tableKey,
+            previousManifests,
+            previousJobIdStr,
+            previousSinkId,
+            operatorId,
+            previousCheckpointId);
     previousCommitter.commit(Sets.newHashSet(new 
MockCommitRequest<>(previousCommittable)));
 
     table.refresh();
@@ -382,6 +401,7 @@ class TestDynamicCommitter {
             newCommitterMetrics);
 
     final String newJobIdStr = newJobId.toHexString();
+    final String newSinkId = UUID.randomUUID().toString();
     final int newCheckpointId = previousCheckpointId + 1;
 
     byte[][] previousManifestsNew =
@@ -396,12 +416,13 @@ class TestDynamicCommitter {
                 tableKey,
                 previousManifestsNew,
                 previousJobIdStr,
+                previousSinkId,
                 operatorId,
                 previousCheckpointId));
     CommitRequest<DynamicCommittable> newCommitRequest =
         new MockCommitRequest<>(
             new DynamicCommittable(
-                tableKey, newManifests, newJobIdStr, operatorId, 
newCheckpointId));
+                tableKey, newManifests, newJobIdStr, newSinkId, operatorId, 
newCheckpointId));
 
     newCommitter.commit(Sets.newHashSet(replayedPreviousCommitRequest, 
newCommitRequest));
 
@@ -411,12 +432,14 @@ class TestDynamicCommitter {
     Snapshot first = Iterables.get(table.snapshots(), 0);
     assertThat(first.summary())
         .containsEntry("flink.job-id", previousJobIdStr)
+        .containsEntry("flink.sink-id", previousSinkId)
         .containsEntry("flink.max-committed-checkpoint-id", 
String.valueOf(previousCheckpointId))
         .containsEntry("flink.operator-id", operatorId);
 
     Snapshot second = Iterables.get(table.snapshots(), 1);
     assertThat(second.summary())
         .containsEntry("flink.job-id", newJobIdStr)
+        .containsEntry("flink.sink-id", newSinkId)
         .containsEntry("flink.max-committed-checkpoint-id", 
String.valueOf(newCheckpointId))
         .containsEntry("flink.operator-id", operatorId);
   }
@@ -441,7 +464,8 @@ class TestDynamicCommitter {
             committerMetrics);
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -460,10 +484,12 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> oldRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, oldManifests, oldJobId, 
operatorId, oldCheckpointId));
+            new DynamicCommittable(
+                tableKey, oldManifests, oldJobId, oldJobId, operatorId, 
oldCheckpointId));
     CommitRequest<DynamicCommittable> newRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, newManifests, newJobId, 
operatorId, newCheckpointId));
+            new DynamicCommittable(
+                tableKey, newManifests, newJobId, newJobId, operatorId, 
newCheckpointId));
 
     // Hand the requests in reversed order; the committer must still land them 
in checkpointId
     // order on the snapshot chain.
@@ -506,7 +532,8 @@ class TestDynamicCommitter {
             committerMetrics);
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -530,7 +557,8 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey, deltaManifests, jobId, jobId, operatorId, 
checkpointId));
 
     // Upgrade the table version
     UpdateProperties updateApi = table1.updateProperties();
@@ -567,7 +595,8 @@ class TestDynamicCommitter {
     TableKey tableKey = new TableKey(TABLE1, "branch");
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -581,7 +610,8 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey, deltaManifests, jobId, jobId, operatorId, 
checkpointId));
 
     dynamicCommitter.commit(Sets.newHashSet(commitRequest));
 
@@ -618,7 +648,8 @@ class TestDynamicCommitter {
     assertThat(table.snapshots()).isEmpty();
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -636,21 +667,24 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest1 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey1, deltaManifests1, jobId, 
operatorId, checkpointId1));
+            new DynamicCommittable(
+                tableKey1, deltaManifests1, jobId, jobId, operatorId, 
checkpointId1));
 
     byte[][] deltaManifests2 =
         aggregator.writeToManifests(tableKey1.tableName(), 
WRITE_RESULT_BY_SPEC_2, checkpointId1);
 
     CommitRequest<DynamicCommittable> commitRequest2 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey1, deltaManifests2, jobId, 
operatorId, checkpointId1));
+            new DynamicCommittable(
+                tableKey1, deltaManifests2, jobId, jobId, operatorId, 
checkpointId1));
 
     byte[][] deltaManifests3 =
         aggregator.writeToManifests(tableKey2.tableName(), 
WRITE_RESULT_BY_SPEC_2, checkpointId2);
 
     CommitRequest<DynamicCommittable> commitRequest3 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey2, deltaManifests3, jobId, 
operatorId, checkpointId2));
+            new DynamicCommittable(
+                tableKey2, deltaManifests3, jobId, jobId, operatorId, 
checkpointId2));
 
     boolean overwriteMode = false;
     int workerPoolSize = 1;
@@ -715,7 +749,8 @@ class TestDynamicCommitter {
     assertThat(table.snapshots()).isEmpty();
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -741,15 +776,18 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest1 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests1, jobId, 
operatorId, checkpointId1));
+            new DynamicCommittable(
+                tableKey, deltaManifests1, jobId, jobId, operatorId, 
checkpointId1));
 
     CommitRequest<DynamicCommittable> commitRequest2 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests2, jobId, 
operatorId, checkpointId2));
+            new DynamicCommittable(
+                tableKey, deltaManifests2, jobId, jobId, operatorId, 
checkpointId2));
 
     CommitRequest<DynamicCommittable> commitRequest3 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests3, jobId, 
operatorId, checkpointId3));
+            new DynamicCommittable(
+                tableKey, deltaManifests3, jobId, jobId, operatorId, 
checkpointId3));
 
     boolean overwriteMode = false;
     int workerPoolSize = 1;
@@ -860,7 +898,8 @@ class TestDynamicCommitter {
     assertThat(table.snapshots()).isEmpty();
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -875,14 +914,16 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest1 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifest1, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey, deltaManifest1, jobId, jobId, operatorId, 
checkpointId));
 
     byte[][] deltaManifest2 =
         aggregator.writeToManifests(tableKey.tableName(), 
WRITE_RESULT_BY_SPEC_2, checkpointId);
 
     CommitRequest<DynamicCommittable> commitRequest2 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifest2, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey, deltaManifest2, jobId, jobId, operatorId, 
checkpointId));
 
     boolean overwriteMode = false;
     int workerPoolSize = 1;
@@ -930,7 +971,8 @@ class TestDynamicCommitter {
     TableKey tableKey = new TableKey(TABLE1, "branch");
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -944,7 +986,8 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, deltaManifests, jobId, 
operatorId, checkpointId));
+            new DynamicCommittable(
+                tableKey, deltaManifests, jobId, jobId, operatorId, 
checkpointId));
 
     dynamicCommitter.commit(Sets.newHashSet(commitRequest));
 
@@ -954,7 +997,7 @@ class TestDynamicCommitter {
     CommitRequest<DynamicCommittable> overwriteRequest =
         new MockCommitRequest<>(
             new DynamicCommittable(
-                tableKey, overwriteManifests, jobId, operatorId, checkpointId 
+ 1));
+                tableKey, overwriteManifests, jobId, jobId, operatorId, 
checkpointId + 1));
 
     dynamicCommitter.commit(Sets.newHashSet(overwriteRequest));
 
@@ -987,7 +1030,8 @@ class TestDynamicCommitter {
     assertThat(table.snapshots()).isEmpty();
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     OneInputStreamOperatorTestHarness aggregatorHarness =
         new OneInputStreamOperatorTestHarness(aggregator);
     aggregatorHarness.open();
@@ -1003,7 +1047,7 @@ class TestDynamicCommitter {
 
     CommitRequest<DynamicCommittable> commitRequest1 =
         new MockCommitRequest<>(
-            new DynamicCommittable(tableKey, manifests, jobId, operatorId, 
checkpointId));
+            new DynamicCommittable(tableKey, manifests, jobId, jobId, 
operatorId, checkpointId));
     Collection<CommitRequest<DynamicCommittable>> commitRequests = 
Sets.newHashSet(commitRequest1);
 
     int workerPoolSize = 1;
@@ -1055,6 +1099,118 @@ class TestDynamicCommitter {
                 .build());
   }
 
+  @Test
+  void testCommitAfterStatelessRestartWithSameJobId() throws Exception {
+    Table table = catalog.loadTable(TableIdentifier.of(TABLE1));
+    DynamicWriteResultAggregator aggregator = openAggregator();
+    TableKey tableKey = new TableKey(TABLE1, "branch");
+    String jobId = JobID.generate().toHexString();
+    String operatorId = new OperatorID().toHexString();
+    String newSinkId = UUID.randomUUID().toString();
+
+    createCommitter()
+        .commit(
+            Sets.newHashSet(
+                commitRequest(
+                    aggregator,
+                    tableKey,
+                    WRITE_RESULT_BY_SPEC,
+                    jobId,
+                    UUID.randomUUID().toString(),
+                    operatorId,
+                    10)));
+
+    createCommitter()
+        .commit(
+            Sets.newHashSet(
+                commitRequest(
+                    aggregator,
+                    tableKey,
+                    WRITE_RESULT_BY_SPEC_2,
+                    jobId,
+                    newSinkId,
+                    operatorId,
+                    1)));
+
+    table.refresh();
+    assertThat(table.snapshots()).hasSize(2);
+    
assertThat(table.snapshot(table.refs().get("branch").snapshotId()).summary())
+        .containsEntry("flink.job-id", jobId)
+        .containsEntry("flink.sink-id", newSinkId)
+        .containsEntry("flink.max-committed-checkpoint-id", "1")
+        .containsEntry("total-records", "66");
+  }
+
+  @Test
+  void testSkipsAlreadyCommittedOlderCommittable() throws Exception {
+    Table table = catalog.loadTable(TableIdentifier.of(TABLE1));
+    DynamicWriteResultAggregator aggregator = openAggregator();
+    TableKey tableKey = new TableKey(TABLE1, "branch");
+    String jobId = JobID.generate().toHexString();
+    String operatorId = new OperatorID().toHexString();
+
+    // Snapshot written before the sinkId was introduced
+    table
+        .newAppend()
+        .appendFile(DATA_FILE)
+        .set("flink.job-id", jobId)
+        .set("flink.operator-id", operatorId)
+        .set("flink.max-committed-checkpoint-id", "5")
+        .toBranch("branch")
+        .commit();
+
+    // Committables from older state use the jobId as sinkId
+    createCommitter()
+        .commit(
+            Sets.newHashSet(
+                commitRequest(
+                    aggregator, tableKey, WRITE_RESULT_BY_SPEC, jobId, jobId, 
operatorId, 5),
+                commitRequest(
+                    aggregator, tableKey, WRITE_RESULT_BY_SPEC_2, jobId, 
jobId, operatorId, 6)));
+
+    table.refresh();
+    assertThat(table.snapshots()).hasSize(2);
+    
assertThat(table.snapshot(table.refs().get("branch").snapshotId()).summary())
+        .containsEntry("flink.sink-id", jobId)
+        .containsEntry("flink.max-committed-checkpoint-id", "6")
+        .containsEntry("total-records", "66");
+  }
+
+  private DynamicWriteResultAggregator openAggregator() throws Exception {
+    DynamicWriteResultAggregator aggregator =
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, SINK_ID);
+    OneInputStreamOperatorTestHarness aggregatorHarness =
+        new OneInputStreamOperatorTestHarness(aggregator);
+    aggregatorHarness.open();
+    return aggregator;
+  }
+
+  private static DynamicCommitter createCommitter() {
+    return new DynamicCommitter(
+        CATALOG_EXTENSION.catalog(),
+        Maps.newHashMap(),
+        false,
+        1,
+        "sinkId",
+        new DynamicCommitterMetrics(new UnregisteredMetricsGroup()));
+  }
+
+  private static CommitRequest<DynamicCommittable> commitRequest(
+      DynamicWriteResultAggregator aggregator,
+      TableKey tableKey,
+      Map<Integer, Collection<WriteResult>> writeResults,
+      String jobId,
+      String sinkId,
+      String operatorId,
+      long checkpointId)
+      throws IOException {
+    byte[][] manifests =
+        aggregator.writeToManifests(tableKey.tableName(), writeResults, 
checkpointId);
+    return new MockCommitRequest<>(
+        new DynamicCommittable(tableKey, manifests, jobId, sinkId, operatorId, 
checkpointId));
+  }
+
   interface CommitHook extends Serializable {
     default Collection<CommitRequest<DynamicCommittable>> beforeCommit(
         Collection<CommitRequest<DynamicCommittable>> commitRequests) {
@@ -1153,11 +1309,20 @@ class TestDynamicCommitter {
         CommitSummary summary,
         String description,
         String newFlinkJobId,
+        String sinkId,
         String operatorId,
         long checkpointId) {
       commitHook.beforeCommitOperation();
       super.commitOperation(
-          table, branch, operation, summary, description, newFlinkJobId, 
operatorId, checkpointId);
+          table,
+          branch,
+          operation,
+          summary,
+          description,
+          newFlinkJobId,
+          sinkId,
+          operatorId,
+          checkpointId);
       commitHook.afterCommitOperation();
     }
   }
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
index 26565089bd..5cdbab9087 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicIcebergSink.java
@@ -460,6 +460,37 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
     runTest(rows);
   }
 
+  @Test
+  void testAllTablesShareTheSinkId() throws Exception {
+    List<String> tableNames = List.of("t1", "t2", "t3", "t4");
+    List<DynamicIcebergDataImpl> rows = Lists.newArrayList();
+    for (String tableName : tableNames) {
+      rows.add(
+          new DynamicIcebergDataImpl(
+              SimpleDataUtil.SCHEMA,
+              tableName,
+              SnapshotRef.MAIN_BRANCH,
+              PartitionSpec.unpartitioned()));
+    }
+
+    executeDynamicSink(rows, env, true, 2, null);
+
+    Set<String> sinkIds = Sets.newHashSet();
+    for (String tableName : tableNames) {
+      Map<String, String> summary =
+          CATALOG_EXTENSION
+              .catalog()
+              .loadTable(TableIdentifier.of(DATABASE, tableName))
+              .currentSnapshot()
+              .summary();
+      assertThat(summary).containsKey("flink.sink-id");
+      
assertThat(summary.get("flink.sink-id")).isNotEqualTo(summary.get("flink.job-id"));
+      sinkIds.add(summary.get("flink.sink-id"));
+    }
+
+    assertThat(sinkIds).hasSize(1);
+  }
+
   @Test
   void testNoShuffleTopology() throws Exception {
     DataStream<DynamicIcebergDataImpl> dataStream =
@@ -2040,11 +2071,13 @@ class TestDynamicIcebergSink extends 
TestFlinkIcebergSinkBase {
           .toBranch(original.key().branch())
           .commit();
 
+      // Committables restored from older state use the jobId as sinkId
       DynamicCommittable replayed =
           new DynamicCommittable(
               original.key(),
               original.manifests(),
               PREVIOUS_JOB_ID,
+              PREVIOUS_JOB_ID,
               original.operatorId(),
               replayedCheckpointId);
       List<Committer.CommitRequest<DynamicCommittable>> enriched =
diff --git 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicWriteResultAggregator.java
 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicWriteResultAggregator.java
index f68e8dae17..3786cea425 100644
--- 
a/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicWriteResultAggregator.java
+++ 
b/flink/v2.3/flink/src/test/java/org/apache/iceberg/flink/sink/dynamic/TestDynamicWriteResultAggregator.java
@@ -64,7 +64,8 @@ class TestDynamicWriteResultAggregator {
     CATALOG_EXTENSION.catalog().createTable(TableIdentifier.of("table2"), new 
Schema());
 
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
cacheMaximumSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, "sinkId");
     try (OneInputStreamOperatorTestHarness<
             CommittableMessage<DynamicWriteResult>, 
CommittableMessage<DynamicCommittable>>
         testHarness = new OneInputStreamOperatorTestHarness<>(aggregator)) {
@@ -107,7 +108,8 @@ class TestDynamicWriteResultAggregator {
     // Disable caching of ManifestOutputFileFactory.
     final int zeroCacheSize = 0;
     DynamicWriteResultAggregator aggregator =
-        new DynamicWriteResultAggregator(CATALOG_EXTENSION.catalogLoader(), 
zeroCacheSize);
+        new DynamicWriteResultAggregator(
+            CATALOG_EXTENSION.catalogLoader(), zeroCacheSize, "sinkId");
     try (OneInputStreamOperatorTestHarness<
             CommittableMessage<DynamicWriteResult>, 
CommittableMessage<DynamicCommittable>>
         testHarness = new OneInputStreamOperatorTestHarness<>(aggregator)) {
@@ -161,7 +163,7 @@ class TestDynamicWriteResultAggregator {
         testHarness =
             new OneInputStreamOperatorTestHarness<>(
                 new DynamicWriteResultAggregator(
-                    CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize))) {
+                    CATALOG_EXTENSION.catalogLoader(), cacheMaximumSize, 
"sinkId"))) {
       testHarness.open();
 
       TableKey tableKey = new TableKey("table", "branch");

Reply via email to