Repository navigation
Flink: Fix duplicate commits in DynamicCommitter when Flink jobId changes on restart - #16011
Conversation
| writeToManifests(entries.getKey().tableName(), entries.getValue(), checkpointId), | ||
| getContainingTask().getEnvironment().getJobID().toString(), | ||
| getRuntimeContext().getOperatorUniqueID(), | ||
| flinkJobId, |
There was a problem hiding this comment.
How did we handle this in the old sink? Would it be confusing if the jobId is not changing with the job?
There was a problem hiding this comment.
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.
|
@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). |
|
CC: @mxm, @Guosmilesmile |
mxm
left a comment
There was a problem hiding this comment.
[..] 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. |
bb80d79 to
76b5f07
Compare
| assertThat(table.snapshots()).hasSize(2); | ||
|
|
||
| Snapshot snapshot1 = Iterables.getFirst(table.snapshots(), null); | ||
| assertThat(snapshot1.snapshotId()).isEqualTo(table.refs().get("branch1").snapshotId()); |
There was a problem hiding this comment.
Checking whether branch1 is always at index 0 of snapshots is wrong and it was making the test flaky
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()); |
|
Is this problem only applicable to the dynamic sink? |
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
|
@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 |
|
@pvary Seems I'm not allowed to merge, can you do it? |
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.
…nges on restart (apache#16011) Cherry-pick of apache#16011. Upstream commit: apache/iceberg@c11404c
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.
…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>
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.