Skip to content

Flink: Fix duplicate commits in DynamicCommitter when Flink jobId changes on restart - #16011

Merged
pvary merged 6 commits into
apache:mainfrom
lrsb:github
Jun 1, 2026
Merged

pvary merged 6 commits into
apache:mainfrom
lrsb:github

Conversation

@lrsb

@lrsb lrsb commented Apr 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #16008

The DynamicCommitter deduplicates commits on the triplet (flink.job-id, flink.operator-id, max-committed-checkpoint-id) stored in each snapshot summary. The aggregator previously sampled the job id from the runtime environment on every operator open(), so a restart or rescale with a fresh Flink JobID would stamp restored write results with the new id. The committer's ancestor walk then could not find the snapshot committed under the old id, and silently re-committed the data, producing duplicate rows in the target table.

Persist the aggregator's job id as operator state and reuse it on restore, so committables keep the job id under which their data was originally produced. This restores the dedup invariant across job restarts, autoscaler savepoint+resubmit cycles, and session-cluster resubmissions, which was broken under sustained catalog-side latency where commit responses were lost client-side.

@github-actions github-actions Bot added the flink label Apr 17, 2026
@lrsb
lrsb marked this pull request as ready for review April 17, 2026 12:41
@lrsb
lrsb marked this pull request as draft April 17, 2026 13:15
@lrsb
lrsb marked this pull request as ready for review April 17, 2026 13:27
writeToManifests(entries.getKey().tableName(), entries.getValue(), checkpointId),
getContainingTask().getEnvironment().getJobID().toString(),
getRuntimeContext().getOperatorUniqueID(),
flinkJobId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did we handle this in the old sink? Would it be confusing if the jobId is not changing with the job?

@Guosmilesmile Guosmilesmile Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found similar logic in SinkV1 for deduplication based on job-id transformation, but I didn't see the related logic in SinkV2. Just like the dynamic sink, it directly obtains a new job-id.

// There're two cases that we restore from flink checkpoints: the first case is restoring from
// snapshot created by the same flink job; another case is restoring from snapshot created by
// another different job. For the second case, we need to maintain the old flink job's id in flink
// state backend to find the max-committed-checkpoint-id when traversing iceberg table's
// snapshots.
private static final ListStateDescriptor<String> JOB_ID_DESCRIPTOR =
new ListStateDescriptor<>("iceberg-flink-job-id", BasicTypeInfo.STRING_TYPE_INFO);

@pvary

pvary commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

@lrsb: Let’s create the PR only for the latest Flink version. After we review and merge it, we can backport the changes to the other Flink versions. This makes it easier for the contributor (no need to keep multiple versions in sync) and for reviewers as well (fewer files to review).

@pvary

pvary commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

CC: @mxm, @Guosmilesmile

@lrsb
lrsb marked this pull request as draft April 21, 2026 07:45

@mxm mxm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[..] so a restart or rescale with a fresh Flink JobID would stamp restored write results with the new id.

Regular job restarts or rescalings do not change the Flink JobID. The JobID only changes on resubmission of the job, which can be triggered by the Kubernetes operator or some other external process.

That said, I'm not convinced we are fixing an actual bug here. DynamicCommitter will commit once the job restarts and restores its state. There will be no new entries in the commit state from the aggregator with the new JobID. The to-be-committed entries after restore will all carry the old JobID. So the logic will work fine as-is.

@lrsb lrsb changed the title Flink: Persist flink job id in DynamicWriteResultAggregator state Flink: Fix duplicate commits when Flink jobId changes on restart Apr 21, 2026
@lrsb
lrsb marked this pull request as ready for review April 21, 2026 12:33
@lrsb

lrsb commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

[..] so a restart or rescale with a fresh Flink JobID would stamp restored write results with the new id.

Regular job restarts or rescalings do not change the Flink JobID. The JobID only changes on resubmission of the job, which can be triggered by the Kubernetes operator or some other external process.

That said, I'm not convinced we are fixing an actual bug here. DynamicCommitter will commit once the job restarts and restores its state. There will be no new entries in the commit state from the aggregator with the new JobID. The to-be-committed entries after restore will all carry the old JobID. So the logic will work fine as-is.

Regarding the job id change during rescalings, that happens when the scaling is applied using stop with savepoint and then the parallelism is overridden at vertex level, without involving the stock Flink autoscaler. I agree this is not the standard setup but might be a possibility when the autoscaler cannot be used.

@mxm mxm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lrsb! The new solution looks good.

@lrsb
lrsb force-pushed the github branch 2 times, most recently from bb80d79 to 76b5f07 Compare April 21, 2026 15:56
assertThat(table.snapshots()).hasSize(2);

Snapshot snapshot1 = Iterables.getFirst(table.snapshots(), null);
assertThat(snapshot1.snapshotId()).isEqualTo(table.refs().get("branch1").snapshotId());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this check removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking whether branch1 is always at index 0 of snapshots is wrong and it was making the test flaky

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commitRequest1 and commitRequest2 creates branch1, and they are both for checkpointId1, only commitRequest3 should create branch2 which is checkpointId2.
How could it happen that branch2 is the first snapshot?
Can we check the history, that the commits are in order as we expect them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could it happen that branch2 is the first snapshot?

It can happen depending on the order of the commit request map iterable (basically the hashcode).

Can we check the history, that the commits are in order as we expect them?

I'd rather not add a dedicated history-order assertion as it would be testing the HashMap sort contract rather than user visible behavior, and testSkipsAlreadyCommittedDataAfterJobIdChanges already covers the dedup-correctness invariant under mixed jobIds

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the new code could result in cases where the commit from checkpoint 2 is added to the table sooner than a commit for checkpoint 1.

I can accept that this is ok, if the commits are for different branches. The question is: do we have tests that ensure that if we commit them in the same cycle, they are committed in the expected order?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is: do we have tests that ensure that if we commit them in the same cycle, they are committed in the expected order?

The order between different branches or different tables depends on the HashMap order, which can reorder a ckpt=2 branch-B commit before a ckpt=1 branch-A commit.

The question is: do we have tests that ensure that if we commit them in the same cycle, they are committed in the expected order?

The order of commits within the same cycle is currently not covered well, testSkipsAlreadyCommittedDataAfterJobIdChange covers just the single commit case. Will add a new test.

.build());

Snapshot snapshot2 = Iterables.get(table.snapshots(), 1);
assertThat(snapshot2.snapshotId()).isEqualTo(table.refs().get("branch2").snapshotId());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this check removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

@stevenzwu

Copy link
Copy Markdown
Contributor

Is this problem only applicable to the dynamic sink?

lrsb added 5 commits May 27, 2026 18:14
DynamicCommitter dedupes pending commit requests against the table's
ancestor snapshots by looking up snapshots whose FLINK_JOB_ID and
OPERATOR_ID match the committable that produced them. The lookup used a
single (jobId, operatorId) pair picked from the last committable in the
batch, which breaks down when the batch spans multiple Flink jobIds.

After a stop-with-savepoint, the committer's state replays in-flight
committables tagged with the previous Flink jobId alongside fresh
committables tagged with the new jobId. Using only the new jobId for
dedup hides the snapshot that the previous jobId already produced, so
the replayed committable's data files are committed a second time and
the affected rows appear twice in the target table.

Partition commit requests by (table, branch, jobId, operatorId) so each
group is deduped and stamped with its own jobId. The downstream commit
pipeline is unchanged; it already assumes a single (jobId, operatorId)
per batch, which the new grouping restores in the mixed-jobId case.

Change-Id: I2e0322a8f6564f960dd5c4730009c20e533223db
@lrsb

lrsb commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@stevenzwu The static IcebergFilesCommitter (in Sink V1) persists the previous flink.job-id in operator state and restores it on initializeState, so committables produced after a restart still carry the original jobId and the dedup walk finds the prior snapshot.

It's explicitly stated here

// There're two cases that we restore from flink checkpoints: the first case is restoring from
// snapshot created by the same flink job; another case is restoring from snapshot created by
// another different job. For the second case, we need to maintain the old flink job's id in flink
// state backend to find the max-committed-checkpoint-id when traversing iceberg table's
// snapshots.
private static final ListStateDescriptor<String> JOB_ID_DESCRIPTOR =
new ListStateDescriptor<>("iceberg-flink-job-id", BasicTypeInfo.STRING_TYPE_INFO);

@stevenzwu stevenzwu changed the title Flink: Fix duplicate commits when Flink jobId changes on restart Flink: Fix duplicate commits in DynamicCommitter when Flink jobId changes on restart May 28, 2026

@pvary pvary left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 pending tests

@lrsb

lrsb commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@pvary Seems I'm not allowed to merge, can you do it?

@pvary
pvary merged commit c11404c into apache:main Jun 1, 2026
34 of 36 checks passed
@pvary

pvary commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Merged to main.
Thanks @lrsb for the fix and all of the reviewers!

@lrsb: Please don't forget to create the backport PRs.
This could help:

git diff HEAD^ HEAD flink/v2,1 |sed "s/v2.1/v2.0/g">/tmp/patch;g apply -3 -p1 /tmp/patch

lrsb added a commit to lrsb/iceberg that referenced this pull request Jun 1, 2026
@lrsb

lrsb commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@pvary backported in #16648

huaxingao pushed a commit that referenced this pull request Jun 1, 2026
@nssalian nssalian added this to the Iceberg 1.12.0 milestone Jul 6, 2026
nickwu241 pushed a commit to Affirm/iceberg that referenced this pull request Sep 8, 2026
nickwu241 added a commit to Affirm/iceberg that referenced this pull request Sep 8, 2026
Fix duplicate commits in DynamicCommitter when Flink jobId changes on
restart (e.g. stop-with-savepoint, autoscaler resubmit).

flink/v2.0's DynamicCommitter/TableKey/DynamicCommittable were
byte-identical to flink/v2.1's pre-cherry-pick versions on this branch
(both already on Iceberg 1.11.0), so this is a straight mirror of the
v2.1 cherry-pick in the previous commit rather than a manual API
adaptation.
nickwu241 pushed a commit to Affirm/iceberg that referenced this pull request Sep 8, 2026
nickwu241 added a commit to Affirm/iceberg that referenced this pull request Sep 8, 2026
Backport of apache#16011
(apache/iceberg@c11404c) onto flink/v2.0.

Fix duplicate commits in DynamicCommitter when Flink jobId changes on
restart (e.g. stop-with-savepoint, autoscaler resubmit).

flink/v2.0's DynamicCommitter/TableKey/DynamicCommittable were
byte-identical to flink/v2.1's pre-cherry-pick versions on this branch
(both already on Iceberg 1.11.0), so this is a straight mirror of the
v2.1 cherry-pick in the previous commit rather than a manual API
adaptation.
nickwu241 pushed a commit to Affirm/iceberg that referenced this pull request Sep 8, 2026
nickwu241 pushed a commit to Affirm/iceberg that referenced this pull request Sep 8, 2026
nickwu241 added a commit to Affirm/iceberg that referenced this pull request Sep 22, 2026
…licate commits in DynamicCommitter when Flink jobId changes on restart (#10)

* Flink: Fix duplicate commits in DynamicCommitter when Flink jobId changes on restart (apache#16011)

Cherry-pick of apache#16011.

Upstream commit: apache/iceberg@c11404c

* Flink: Backport DynamicCommitter jobId fix to v1.20 and v2.0

Backport commit apache#16011.

* Pin version.txt to 1.11.0-affirm-patch.1 and add AFFIRM_VERSIONS.md

version.txt was previously untracked, so build.gradle's getProjectVersion()
silently falls back to a git-describe-derived version if the file is ever
missing from the working tree (e.g. after a checkout/clean) instead of
failing loudly. This has already caused one unintended publish under
1.10.0-SNAPSHOT.

AFFIRM_VERSIONS.md records which Affirm-local patches are carried by each
published version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Remove published commit tracking from AFFIRM_VERSIONS.md

Git tags are the source of truth for the commit published to Artifactory, not this file.

* Document PR #11 (MinIO/Quay CI fix) in AFFIRM_VERSIONS.md

Records the Docker Hub -> Quay MinIO fix as part of 1.11.0-affirm-patch.1
now that it's merged onto 1.11.0-affirm-patch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: lrsb <github@lrsb.xyz>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

7 participants