Skip to content

[Feature][Connector-V2][Iceberg] Support v3 CDC and replay-safe commits - #12881

Open
goutamadwant wants to merge 8 commits into
apache:devfrom
goutamadwant:feature/iceberg-v3-cdc
Open

goutamadwant wants to merge 8 commits into
apache:devfrom
goutamadwant:feature/iceberg-v3-cdc

Conversation

@goutamadwant

@goutamadwant goutamadwant commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Purpose of this pull request

Related to #12763. This is the second Iceberg v3 implementation slice after #12777; it does not close the remaining v3 roadmap.

  • Enable v3 CDC/upsert writes using equality deletes for previously committed rows and Puffin deletion vectors for invalidated rows in the current writer's new files.
  • Support Parquet, ORC and Avro data files without scanning existing files or rewriting their deletion vectors.
  • Recognize already-published writer results in both ordinary and restore commits using immutable payload fingerprints, not checkpoint numbers alone.
  • Preserve writer-generation ordering across schema changes so later equality deletes remove rows from earlier generations, for both v2 and v3.
  • Add nanosecond equality-delete reader support, reused-Avro-buffer regression coverage, recovery fixtures and bilingual documentation.

Does this PR introduce any user-facing change?

Yes. Format-version 3 CDC/upsert configurations become supported. Format version 2 remains the default and keeps its existing writer selection.

For both v2 and v3 CDC/upsert jobs, a checkpoint spanning writer generations can publish several ordered snapshots when needed for equality-delete ordering. External readers may observe a committed prefix between groups. Single-generation and append-only checkpoints retain their single-snapshot behavior. Both incompatible-changes documents explain this compatibility impact.

The result-list index is each writer's chronological generation, not a global order between independent writers. Existing key-affinity, ordering and partition-key requirements remain necessary. Group ordinals/counts are diagnostic metadata, not evidence that a payload was published.

No new dependency, configuration option, public API or checkpoint serialization format is introduced. Recovery is scoped to the configured table branch and requires retained snapshot history, manifests and pending files, plus engine fencing against overlapping attempts publishing the same payload. Partial legacy matches and manifest-read failures fail closed. Recovery after the necessary evidence expires is not covered.

Overlap with #12937: both changes touch generation grouping and the same commit-group summary keys. This PR is self-contained; whichever lands second must reconcile with the landed implementation rather than introduce a second grouping rule. No unmerged #12937 code is included here.

Variant/default values, row-lineage exposure, incremental v3 reads and multi-argument transforms remain separate roadmap slices.

How was this patch tested?

The current connector suite passed on JDK 11 and JDK 17: 187 reported tests per runtime, 186 executed and passed, and one existing skipped metastore test. Scoped Spotless, verification and packaging passed on JDK 11. The suite covers ordered publication and replay for v2/v3, repeated updates/deletes, schema and partition/key changes, file rolling, checkpoint-ID reuse, regrouped committables, branch isolation, legacy publication checks and deletion-vector identity.

Earlier recovery evidence, not a claim that the entire matrix was rerun on the current head:

  • Zeta v2/v3 recovery passed after actual single-node server-JVM termination and explicit checkpoint restore with the unchanged upstream harness. This is not HA failover coverage.
  • Broader Flink testing covered v2/v3 crashes before and after external publication. Seven cases passed; the remaining worker-restart fixture was corrected and the v2 pre-publication case rerun successfully on Flink 1.18 and 1.20. The complete eight-case matrix was not rerun on the final artifact.
  • The corrected fixture waits for a different worker registration, verifies one restore from the original checkpoint, completes fresh checkpoints and checks exact rows/files/snapshots.
  • Flink 1.20 required local platform and native-G1-thread harness corrections on the ARM host. Neither shared-harness correction is included here; [Fix][E2E] Build the Java 11 engine images with a JDK of the engine image's platform #12855 separately tracks the image-platform change.
  • A real Avro regression reproduced a deleted row surviving when nanosecond equality keys reused binary buffers. Copying retained normalized keys fixes that regression.

Full-reactor execution, Spark recovery, native Windows, cloud catalogs and a fresh complete Docker recovery matrix are not claimed. Hosted CI and maintainer approval remain separate gates.

Check list

  • New Jar binary package and License/NOTICE updates: not applicable; no dependency is added.
  • English and Chinese documentation updated.
  • incompatible-changes.md updated in both languages for ordered multi-snapshot publication and its visible-prefix boundary.
  • Connector integration reviewed: E2E coverage added; existing plugin mapping, distribution registration, CI labels and plugin configuration remain unchanged.

… commits

Use equality deletes for committed rows and Puffin deletion vectors for invalidated rows in new files. Make ordinary and restore commits recognize immutable writer payloads without treating checkpoint numbers as publication proof.

Add v2/v3 recovery fixtures for Flink and Zeta, nanosecond equality-delete reader coverage, and matching English and Chinese documentation. Related to apache#12763.

Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>
@goutamadwant

Copy link
Copy Markdown
Collaborator Author

The red build is due to the existing JDBC JdbcOptionRuleCompletenessTest failure, unrelated to this PR and addressed by #12875. Iceberg integration jobs passed on Java 11/17, including all 8 Flink and 2 Zeta recovery cases confirmed in the Java 17 logs. thanks!

@DanielLeens DanielLeens 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 for the substantial contribution. I reviewed the complete current diff and traced the writer, commit, recovery, and read paths on the latest head.

What Problem Does This PR Solve?

  • User pain point: Iceberg format-version 3 tables reject the connector's former position-delete path for CDC/upsert, and a recovered committable can be replayed through the ordinary commit path.
  • Fix approach: select a v3 deletion-vector writer, retain equality deletes for already committed rows, and fingerprint immutable writer payloads in snapshot metadata so recovery can recognize a published payload.
  • One-sentence summary: this enables v3 CDC/upsert and makes checkpoint replay identify the payload rather than trusting a reusable checkpoint number.

1. Code Change Review

1.1 Core Logic Analysis

The critical path starts at IcebergWriterFactory.createTaskWriter: delta writes on format v3 select DeletionVectorTaskWriter (IcebergWriterFactory.java:151-177). Its per-partition writer records the position of each newly written key; a replacement or delete in that same writer emits a Puffin deletion vector, while a key not produced by that writer goes to an equality-delete writer (DeletionVectorTaskWriter.java:289-322). The checkpoint then reaches IcebergAggregatedCommitter.commit, which groups committables by checkpoint and invokes IcebergFilesCommitter (IcebergAggregatedCommitter.java:55-76).

Before, v3 delta writes were rejected before any data was produced. After, the committer creates a stable fingerprint from immutable data/delete-file identities, stores it in the committed snapshot, and reuses the same pending-payload check from both normal and restore commits (IcebergFilesCommitter.java:85-160, 281-353). The reader-side change additionally normalizes nanosecond timestamp equality-delete keys before filtering records (IcebergFileScanTaskReader.java:102-146).

Key findings:

  • The normal streaming path reaches the new v3 writer whenever identifier fields or upsert mode make the write a delta write.
  • The implementation deliberately uses deletion vectors only for rows produced by the current task writer; prior rows remain equality-delete targets.
  • Recovery is replay-safe by payload fingerprint, so another job's reused checkpoint number is not treated as proof of publication.
  • Legacy snapshots without a fingerprint fail closed on a partial match rather than silently dropping or duplicating a payload.

Runtime path:

Source/transform emits RowKind
  -> IcebergWriterFactory.createTaskWriter()
  -> format v3 delta write -> DeletionVectorTaskWriter.write()
      -> same-writer replacement/delete: Puffin deletion vector
      -> prior-row delete: equality delete
  -> checkpoint result -> IcebergAggregatedCommitter.commit()
  -> IcebergFilesCommitter.pendingResults()
      -> fingerprint/history verification
      -> atomic AppendFiles or RowDelta commit
  -> recovery replay -> ordinary commit() -> same verification
  -> IcebergFileScanTaskReader -> equality-delete normalization/filtering

1.2 Compatibility Impact

Fully compatible. Existing v1/v2 write selection and configuration names remain intact. Format v3 changes from a documented early rejection to supported CDC/upsert behavior, while format v4 and later remain explicitly rejected. No API, protocol, serialization format, or default-value change was found. The English and Chinese connector documentation describe the new operational and recovery constraints consistently.

1.3 Performance / Side-Effect Analysis

Normal fingerprint-matched replays and new positive-ID commits avoid a live-manifest scan. Legacy unmatched payloads can read history and, with a broken ancestry chain and a non-positive checkpoint ID, scan live manifests; the documentation now calls out that compatibility cost. The writer keeps only per-key positions for the current task writer, releases writer resources during close/abort, and propagates close/manifest errors rather than reporting a successful commit.

1.4 Error Handling and Logging

No formal source issue found. The writer retains the first close failure and suppresses subsequent close failures, abort cleans up files created by the attempt, and publication-state read failures are propagated. No sensitive values are logged.

2. Code Quality Assessment

2.1 Coding Standards

The new production class has the ASF header and documents its v3 boundary and cleanup behavior. The change is localized to the Iceberg writer, committer, reader, tests, E2E fixtures, and paired documentation.

2.2 Test Coverage and Test Stability

Coverage includes writer-result fingerprinting, reused checkpoint IDs, legacy-history behavior, shared Puffin deletion-vector identities, reader timestamp-nanos equality deletes, and Flink/Zeta recovery scenarios. The recovery E2Es use Awaitility with bounded condition checks rather than fixed sleeps; temporary paths are UUID-scoped and the assertions verify committed rows, snapshots, and deletion-vector metadata.

Stability rating: Stable. IcebergFlinkRecoveryIT.java:215-350 and IcebergZetaRecoveryIT.java:121-177 wait on observed job/checkpoint state and committed rows; no fixed wait, hard-coded host port, or shared mutable test state was found in the changed tests.

2.3 Documentation Updates

Required documentation was updated in both docs/en/connectors/sink/Iceberg.md and docs/zh/connectors/sink/Iceberg.md. The examples and constraints match the implementation, including the partition-key and checkpoint-recovery limitations.

3. Architectural Soundness

3.1 Elegance of the Solution

Long-term solution. The writer selection is constrained by Iceberg table format, and replay protection is stored beside the snapshot publication it proves.

3.2 Maintainability

The writer/committer boundary is explicit: writers create immutable files, while the committer owns publication verification. The legacy path is isolated and documented.

3.3 Extensibility

Versioned snapshot fingerprints permit future fingerprint evolution without reintroducing checkpoint-number identity assumptions.

3.4 Historical-Version Compatibility

Historical snapshots remain readable. Their recovery evidence is evaluated conservatively, and partially published legacy payloads fail rather than being guessed.

4. Issue Summary

Number Issue Location Severity
— No formal source issue found — —

5. Merge Recommendation

Conclusion: Ready to merge after fixes

  1. Blockers — must be fixed

    • The current Build gate is red in the four unit-test matrix jobs (Java 11/17 on Ubuntu and Windows). The Iceberg integration jobs passed. The contributor identifies the failure as the pre-existing JdbcOptionRuleCompletenessTest issue addressed by #12875; that PR is still open and is not in dev. Please wait for that fix to enter dev, sync this branch, and rerun the failed unit-test matrix. A sync before #12875 merges will not resolve this particular gate.
  2. Recommended fixes — non-blocking

    • None.

Overall assessment: the source change is a well-scoped long-term implementation with strong unit and recovery-E2E coverage. I found no source-side blocker in the current head. Once the external unit-test gate is green, the source is ready from my technical-review perspective. I have read-only review controls here, so this COMMENT does not change GitHub's visible review decision; a write-capable maintainer must perform any final approval action.

nzw921rx
nzw921rx previously approved these changes Oct 19, 2026

@nzw921rx nzw921rx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1 LGTM

if CI pass

@zhengxiang378928908-code

Copy link
Copy Markdown
Contributor

@goutamadwant thanks for this. I'm the author of #12937 (flush_row_size), which also changes the committers. While checking the overlap, I reproduced a stale-row case at 427c77210 on both format v2 and v3.

In a connector-level test with an unpartitioned, auto-created table, primary key id, and upsert enabled, I ran:

  1. INSERT (1, a)
  2. applySchemaChange adding a nullable note column
  3. UPDATE_AFTER (1, a2, n)
  4. complete(), followed by one aggregated commit of the returned results

Reading the table returns both 1=a and 1=a2. The snapshot summary contains added-data-files=2, added-equality-deletes=2, and total-position-deletes=0. I checked that the tables were actually format v2 and v3. This exercises the writer/committer path directly, not a running engine checkpoint.

applySchemaUpdate completes the first task writer and creates a new one. The second writer has no record of the earlier row's position, so it writes an equality delete. When both results are committed in the same snapshot, that delete does not apply to the earlier data file because they have the same data sequence number.

The v2 case already exists on dev; #12937 addresses it with ordered snapshots, and its schemaChangeInsideCheckpointKeepsUpsertCorrect test passes for v2. This PR enables the analogous v3 case. DeletionVectorTaskWriter tracks positions within one task-writer instance, including its rolled files, but that state does not carry across the schema-change reset. I have not tested a combined #12881 + #12937 implementation on v3.

There is also a recovery integration point. #12937 tags split checkpoints with seatunnel.commit-group / seatunnel.commit-group-count and skips published groups during restore. For positive checkpoint IDs, that detection uses checkpoint/group metadata; for non-positive IDs it checks file paths. This PR instead recognizes individual writer results using seatunnel.commit-ids, with publication checks shared by ordinary commit and restore.

When combining the changes, I suggest retaining ordered groups and recording the fingerprints of each group's results on that group's snapshot, while preserving publication checks on both ordinary commit and restore. We should test recovery after only the first group is published, replay through ordinary commit, and checkpoint-ID reuse. I haven't validated that combined behavior yet.

I'm happy to rebase #12937 onto this PR if that is the preferred landing order. @DanielLeens, does that approach make sense?

@DanielLeens

Copy link
Copy Markdown
Contributor

Sorry for missing this in my previous review. This is a carry-over omission in my earlier source-clear conclusion, not an issue introduced by a new commit: the head is still 427c77210473ed8b3665559487949eb8816ed1c9. +1 to @zhengxiang378928908-code's concrete schema-change/upsert counterexample, and thank you for distinguishing the connector-level reproduction from an engine E2E.

I fetched that head and traced the relevant path:

IcebergSinkWriter.applySchemaChange (IcebergSinkWriter.java:121-133)
  -> IcebergRecordWriter.applySchemaUpdate (:155-161)
       flush() completes and saves the old task-writer result
       resetWriter() creates a new task writer
  -> new DeletionVectorTaskWriter.PartitionWriter has a fresh rows map (:279-283)
       UPDATE_AFTER -> delete(key) -> no previous position -> equality delete (:306-321)
       insert(updated row)
prepareCommit -> IcebergRecordWriter.complete (:165-169)
  -> both writer results
IcebergAggregatedCommitter.commitFiles / flattenResults (:92-103)
  -> IcebergFilesCommitter.commit (:342-353)
       one RowDelta adds both results' data and delete files, then commits once

That supports the reported stale-row outcome: the new writer no longer knows the pre-schema-change row's file/position, and an equality delete does not remove a data file with the same data sequence number. The Iceberg scan-planning contract requires an older data sequence for an equality delete to apply. Deletion vectors would handle same-commit positions, but the earlier writer's positions are not carried across this reset. The v2 case is pre-existing; enabling the analogous v3 path makes it relevant to this PR as well.

I withdraw the earlier CI-only/source-clear conclusion. This is a P0 data-correctness blocker for the schema-change + upsert path when those results reach one commit. I did not run your reproduction locally, and I am not claiming every Zeta schema-change sequence reaches the window. The connector contract is affected, and the Flink sink schema-event handler also calls applySchemaChange directly without an intervening prepareCommit in that handler; an engine regression should pin the intended supported path rather than relying on an implicit barrier assumption.

Yes, preserving ordered groups and placing each group's result fingerprints on that group's snapshot is the right direction. Both ordinary commit and restore must share the publication check; a reused checkpoint number must not identify a different payload as already published. Please retain the exact final-row assertion for v2/v3 across a schema change, and cover a crash after only the first group is published, replay through ordinary commit, and checkpoint-ID reuse. A combined tree has not been validated by this reply.

For landing order, please coordinate the two PRs so the ordering fix is present before the newly supported v3 schema-change/upsert combination is mergeable. Merging this PR first while leaving that correctness gap for a later PR would not be safe. A prerequisite/common correctness change, or explicitly keeping the unsupported combination gated until it lands, is preferable. This reply answers the existing counterexample and corrects my prior conclusion; it is not a claim that a fresh full review of all 21 files or a combined v3 E2E has been completed.

@DanielLeens DanielLeens 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 for the update, @goutamadwant. The head moved from 427c7721 to f391f0d2 only by merging the latest dev (0 commits behind now). I compared the PR's own change set (21 files, 3842 insertions, 373 deletions) against the merge base before and after the merge and it is identical, so nothing in the code has changed in response to the open correctness finding below.

What Problem Does This PR Solve?

Iceberg format-version 3 tables reject the former position-delete path for CDC/upsert, and a recovered committable can be replayed through the ordinary commit path. The PR adds a v3 deletion-vector writer, keeps equality deletes for already committed rows, and fingerprints writer payloads in snapshot metadata so recovery can recognize a published payload.

1. Code Change Review

1.1 Core Logic Analysis

Unchanged from my last review. Delta writes on format v3 select DeletionVectorTaskWriter (IcebergWriterFactory.java); committables flow through IcebergAggregatedCommitter.commit into IcebergFilesCommitter, which verifies the payload fingerprint before an atomic AppendFiles or RowDelta commit.

1.2 Compatibility Impact

Fully compatible for existing v1/v2 behavior and configuration. Format v3 changes from early rejection to supported CDC/upsert. No API or serialization change.

1.3 Performance / Side-Effect Analysis

Unchanged: fingerprint-matched replays avoid a live-manifest scan; the legacy path is documented.

1.4 Error Handling and Logging

Close and publication-state failures are propagated. One correctness issue is open (carried over from the thread, and acknowledged in my comment of 2026-10-22):

Issue 1

  • Location: IcebergRecordWriter.applySchemaUpdate (flush then resetWriter), DeletionVectorTaskWriter partition writer state, IcebergFilesCommitter single RowDelta commit
  • Problem: after a schema change the new task writer has no memory of earlier rows' file positions, so UPDATE_AFTER for a key written before the schema change becomes an equality delete. Both writer results are committed in one snapshot, so the equality delete has the same data sequence number as the earlier data file and does not apply. @zhengxiang378928908-code reproduced this at the connector level on v2 and v3 (INSERT, schema change, UPDATE_AFTER, complete(), one aggregated commit): the table returns both 1=a and 1=a2.
  • Potential risk: silent stale rows (data correctness) for the schema-change plus upsert combination on the newly supported v3 path. The v2 variant already exists on dev and is addressed by #12937.
  • Best improvement: commit the results of each writer generation as ordered groups (distinct snapshots) so equality deletes apply, record each group's result fingerprints on that group's snapshot, and share the publication check between ordinary commit and restore; coordinate landing order with #12937, or gate the v3 schema-change/upsert combination until the ordering fix is present. Add exact final-row assertions for v2 and v3 across a schema change, plus a crash after only the first group is published, replay through ordinary commit, and checkpoint-ID reuse.
  • Severity: High
  • Raised by another reviewer: Yes (@zhengxiang378928908-code); I am +1 on the finding.

2. Code Quality Assessment

2.1 Coding Standards

Unchanged: ASF headers and Javadoc are in place.

2.2 Test Coverage and Test Stability

Existing unit and recovery E2E coverage is solid, but none of it covers a schema change followed by an update in the same commit, which is why this slipped through. Stability: Stable for the existing tests; the missing case must be added.

CI for f391f0d2: the fork Build run is still in progress (its changes and Code style jobs passed). The earlier red unit-test legs were the dev regression JdbcOptionRuleCompletenessTest (#12874), and its fix #12875 is still open. Syncing before #12875 lands will not clear that gate.

2.3 Documentation Updates

EN and ZH Iceberg sink docs are updated consistently; they should also state the schema-change/upsert limitation or behavior once the ordering is resolved.

3. Architectural Soundness

The writer/committer split and snapshot-stored fingerprints remain the right design. The open point is that publication ordering across writer generations must be part of that design, and it overlaps with #12937 (seatunnel.commit-group metadata), so the two PRs need one agreed grouping and fingerprint scheme.

4. Issue Summary

Number Issue Location Severity
1 Schema change then upsert in one commit leaves stale rows (equality delete not applied) IcebergRecordWriter.applySchemaUpdate, DeletionVectorTaskWriter, IcebergFilesCommitter High

5. Merge Recommendation

Conclusion: Not recommended for merge

  1. Blockers - must be fixed
    • Issue 1: ordered commit groups (or an explicit gate for v3 schema-change plus upsert) with the final-row and recovery tests listed above, coordinated with #12937.
    • CI: wait for the in-progress fork Build of this head; the dev JDBC break (#12875) must be resolved before the unit-test matrix can go green.
  2. Recommended non-blocking fixes
    • Document the schema-change/upsert behavior in the sink docs.

Overall assessment: the v3 writer and replay-safe commit design is sound, but I withdrew my earlier source-clear conclusion because of Issue 1. The latest merge changed nothing relevant, so the verdict stays the same until the fix lands.

@github-actions github-actions Bot removed the reviewed label Oct 22, 2026
…replay

Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>
Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>
@zhengxiang378928908-code

Copy link
Copy Markdown
Contributor

@goutamadwant I reran my connector-level reproduction (INSERT, applySchemaChange adding a nullable column, UPDATE_AFTER, then one aggregated commit) on 311b653ab. On both format v2 and v3 the table now reads back only 1=a2. The commit is split into two snapshots, and the second one carries seatunnel.commit-group=1 / seatunnel.commit-group-count=2. IcebergAggregatedCommitterTest also passes locally. I have not tested recovery after only the first group is published.

@DanielLeens DanielLeens 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.

Thank you @goutamadwant for the focused fix in beeda6fac, and @zhengxiang378928908-code for re-running your reproduction on the new head and posting the result. I re-reviewed the full PR at head 311b653abc12cbf2db7188ff5fbe21b8787ceb11, not only the increment. Since my last review, the head gained one real commit (beeda6fac, "Preserve writer generation ordering during checkpoint replay") plus a merge of dev.

The High-severity blocker from my previous review is resolved. The stale-row case (INSERT, schema change, UPDATE_AFTER, one aggregated commit) no longer reaches a single snapshot. IcebergAggregatedCommitter now publishes writer generations as ordered commit groups, so an equality delete from a later generation always gets a newer data sequence number than the rows it must remove. The fix covers both format v2 and v3, and it comes with exactly the tests I asked for: an exact final-row assertion for v2 and v3, a crash after only the first group is published (replayed through both ordinary commit and restore), and checkpoint-ID reuse. My CHANGES_REQUESTED was based on that issue only, so from the source side it can now be cleared.

What Problem Does This PR Solve?

  • User pain point: format-version 3 Iceberg tables rejected CDC/upsert writes, a recovered committable could be published twice through the ordinary commit path, and (on dev already, for v2) a schema change inside a checkpoint could leave stale rows.
  • Fix approach: a v3 DeletionVectorTaskWriter (Puffin deletion vectors for rows the current writer produced, equality deletes for older rows); versioned payload fingerprints stored in the published snapshot and checked by both ordinary commit and restore; and now ordered commit groups across writer generations.
  • One-sentence summary: v3 CDC/upsert becomes supported, with replay protection based on the payload and with correct delete ordering across schema changes.

1. Code Change Review

1.1 Core Logic Analysis

New in beeda6fac (IcebergAggregatedCommitter.java:93-142):

// before (dev and the previous head): every result of a checkpoint in one snapshot
filesCommitter.doCommit(flattenResults(commitInfos), checkpointId);

// after: round r = r-th WriteResult of each writer (one per writer generation)
for (round ...) {
    results = r-th result of every writer
    if (round has an equality delete with recordCount > 0 && !pending.isEmpty()) {
        groups.add(pending); pending = new ArrayList<>();   // start a new snapshot
    }
    pending.addAll(results);
}
// groups.size() == 1 -> doCommit(results, id)                (unchanged summary)
// otherwise         -> doCommit(group, id, g, groupCount)    (+ seatunnel.commit-group[-count])

Runtime path:

IcebergSinkWriter.applySchemaChange / auto schema evolution in write()
  -> IcebergRecordWriter.applySchemaUpdate: flush() saves generation g, resetWriter() starts g+1
prepareCommit(checkpointId) -> IcebergRecordWriter.complete() -> [g0, g1, ...] in one IcebergCommitInfo
  -> (checkpoint state; serialized commit info)
IcebergAggregatedCommitter.commit / restoreCommit (same path)
  -> per checkpoint (LinkedHashMap, :58-67) -> orderedGroups()
  -> for each group: IcebergFilesCommitter.pendingResults()  (fingerprints of published results are skipped)
       -> AppendFiles or RowDelta with seatunnel.commit-ids for that group's results

Correctness analysis of the grouping, under the documented key-affinity constraint (all changes of a key go to one writer, in source order):

  • Any equality delete in round r is published in a snapshot strictly after all data of rounds < r. So it now applies to the row that the earlier generation of the same writer wrote. This is the case that was broken.
  • Later rounds without equality deletes are merged into the same snapshot as round r. Their data shares round r's sequence number, so round r's equality deletes do not remove it, which is correct (that data is newer).
  • Position deletes / deletion vectors only reference files of their own generation, so they are valid in any snapshot that also contains those files.
  • Writers with fewer generations (for example, no rows before the schema change) put their only result into round 0. Their keys are disjoint from other writers' keys by the affinity constraint, so this is safe. testWriterGenerationsRemainOrderedAcrossUnevenWorkers pins this behavior.
  • Replay: fingerprints are computed per result, and each group is atomic. After a crash between groups, replaying the same commit info skips group 0 (all its results are found in history) and publishes the remaining groups. recoversAfterPublishingOnlyFirstWriterGeneration shows this for v2 and v3, through both ordinary commit and restore. Group numbers are written but never read for publication decisions, so a reused checkpoint ID cannot make a different payload look published.
  • Multiple checkpoints in one aggregated commit now get one snapshot each, in list order. On dev they shared a single snapshot, which had the same same-sequence-number weakness. This is a side improvement.

The rest of the PR is unchanged since my first review: v3 writer selection in IcebergWriterFactory (format > 3 still rejected), the removal of the old v3 rejection guards in IcebergWriterFactory / SchemaUtils (intended, because the PR enables the feature), the fingerprint-based pendingResults, and the nanosecond equality-delete normalization in IcebergFileScanTaskReader.

1.2 Compatibility Impact

Partially incompatible, and now documented in docs/en|zh/introduction/concepts/incompatible-changes.md. For v2 and v3 CDC/upsert jobs, a checkpoint that contains a schema change can now produce several ordered snapshots instead of one. External readers can see a prefix of that checkpoint between groups. Single-generation and append-only checkpoints keep exactly one snapshot, and the summary keys are unchanged for them (the group properties are added only when groupCount > 1). No option, default, public SPI or checkpoint serialization format changes (IcebergCommitInfo and WriteResult are untouched). Old snapshots stay readable, and legacy, non-fingerprinted history is still evaluated fail-closed.

1.3 Performance / Side-Effect Analysis

orderedGroups is linear in the number of results and delete files of one checkpoint, with no I/O. The extra snapshots appear only for checkpoints with schema changes, so the commit count stays proportional to schema-change frequency, not to data volume. Each extra group runs one more pendingResults history walk. That walk reads snapshot summaries in memory, and the manifest reads are bounded as described in my first review. No new threads, buffers or retries.

1.4 Error Handling and Logging

doCommit(..., group, groupCount) validates its arguments. A failure between groups propagates, and replay completes the remaining groups. The partial-publication guard (alreadyPublished) still fails closed. No sensitive data is logged.

  1. Medium: the new grouping contract has no method-level documentation. Location: IcebergAggregatedCommitter.java:104 (orderedGroups), IcebergFilesCommitter.java:86 (new public doCommit(results, checkpointId, group, groupCount)), :63-64 (the new summary constants). Problem: correctness depends on three non-obvious assumptions. The r-th result of a writer is its r-th writer generation, keys are writer-affine, and the group properties are diagnostic only. Today these are spread over one inline comment and the docs. Risk: a future change to IcebergRecordWriter.complete() (for example the flush_row_size rotation in #12937, which also emits several results per writer) could break the "index = generation" assumption without anyone noticing. Best improvement: a short Javadoc on orderedGroups and the new doCommit overload that states these assumptions, and a one-line comment on the two constants saying they are never used as proof of publication. Raised by another reviewer: no.
  2. Low: the order of multiple checkpoints in one aggregated commit is taken from list order. Location: IcebergAggregatedCommitter.java:58-67. Problem: checkpoints are committed in insertion order. If an engine ever handed over commit infos with checkpoint N+1 before N, N+1's equality deletes would be published first. I did not find a path in Zeta or the Flink translation that does this, and testNativeRebatchingRetainsCheckpointBoundaries covers the ascending case. This is a hardening suggestion, not a finding of a live bug. Best improvement: commit positive checkpoint IDs in ascending order (for example, a TreeMap when every ID is positive), plus a reversed-input test. Raised by another reviewer: no.
  3. Low: the PR description no longer matches the code. Location: PR description. Problem: it still says incompatible-changes.md is "not applicable", and it does not mention that v2 jobs can now get several snapshots per checkpoint. Best improvement: update the checklist and add one sentence about ordered commit groups for v2 and v3. Also mention the overlap with #12937 (same seatunnel.commit-group keys and the same committer files), so maintainers can choose the landing order. Raised by another reviewer: no.

2. Code Quality Assessment

2.1 Coding Standards

The new production code is small and readable, and the inline why-comment in orderedGroups is accurate. Item 1 covers the missing method docs. A minor style point: fully qualified names are used inside method bodies (org.mockito.InOrder at IcebergAggregatedCommitterTest.java:82,196, org.apache.iceberg.DeleteFile at IcebergSinkIT.java:196) where a normal import would do.

2.2 Test Coverage and Test Stability

  • preservesUpsertOrderingAcrossSchemaChange (IcebergFormatVersionTest.java:1097, v2 and v3): one real writer, INSERT, applySchemaChange, UPDATE_AFTER, then a real commit against a local catalog. It asserts exactly one live row with the new values, two snapshots and the group properties. Then it replays through commit and restore without creating new snapshots, and checks that a different payload under the same checkpoint ID is still published. If the grouping is reverted to a single snapshot, this test reads two live rows and fails.
  • recoversAfterPublishingOnlyFirstWriterGeneration (:1145, v2 and v3 x ordinary/restore) makes the second newRowDelta() fail. It asserts that only the first group is visible, and that replay publishes exactly the missing group once.
  • testWriterGenerationsRemainOrderedAcrossUnevenWorkers and testAppendOnlyWriterGenerationsKeepSingleCommit pin the grouping and the single-snapshot fallback (exact doCommit arguments, in order).
  • Removed tests: the old checkpoint-ID-based isAlreadyCommitted* and restore-skip tests were replaced by stronger, real-table tests of the payload-based semantics (15 new cases in IcebergFilesCommitterTest). That is consistent with the intended change, since a checkpoint number is no longer proof of publication. IcebergSinkParallelCommitIT only changed comments and messages, and its >= 2 snapshot assertion is unchanged. The v3 rejection E2E became a positive v3 CDC E2E with exact rows and deletion-vector metadata. I did not find any weakened assertion.
  • Stability rating: Stable. The new unit tests use a temporary local catalog and deterministic Mockito spies, with no sleeps or ports. The existing Flink/Zeta recovery E2Es wait on observed state.

2.3 Documentation Updates

The EN and ZH sink pages and the incompatible-changes.md entries are updated consistently and match the code. They cover ordered groups for v2 and v3, the visible prefix between groups, the single-snapshot cases, and the note that group numbers are not publication proof. The earlier "checkpoints commit everything together" sentence was correctly removed.

3. Architectural Soundness

  • 3.1 Elegance: Long-term solution. Ordering is fixed where publication happens (the aggregated committer), and replay safety remains payload-based. Nothing is added to the engine or the API.
  • 3.2 Maintainability: good, once item 1 documents the "result index = writer generation" contract.
  • 3.3 Extensibility: #12937 (flush_row_size) solves the same ordering problem for memory-driven rotation, with the same summary keys. The two PRs now overlap directly, and the second one to land will need to adopt the other's grouping. I recommend that maintainers decide the landing order explicitly. This PR is now self-contained and safe to land first.
  • 3.4 Historical-Version Compatibility: checkpoint state classes are unchanged, so in-flight checkpoints from the previous version restore and replay through the new path. Old snapshots without fingerprints keep the fail-closed legacy check.

4. Issue Summary

Number Issue Location Severity
1 Grouping contract (index = generation, key affinity, group keys diagnostic) not documented on the new methods IcebergAggregatedCommitter.java:104, IcebergFilesCommitter.java:63-64,86 Medium
2 Multiple checkpoints in one aggregated commit are ordered by list order, not checkpoint ID (hardening) IcebergAggregatedCommitter.java:58-67 Low
3 PR description outdated (incompatible-changes, v2 multi-snapshot, #12937 overlap) PR description Low
- Previous Issue 1: schema change then upsert in one commit left stale rows IcebergAggregatedCommitter, IcebergFilesCommitter Fixed in beeda6fac

5. Merge Recommendation

CI: the fork Build for this head (run 40077518263) and the apache-side Build check are both still queued, so there is no result for 311b653ab yet. The branch is level with dev (ahead 4, behind 0), with no conflicts. Please let the run finish. If a lane fails, tell me which one, and I will check whether it relates to this diff.

Conclusion: Ready to merge

  1. Blockers (must fix): none from the source, tests or docs. No blocking issues found. The remaining gate is a green Build on this head.
  2. Recommended fixes (non-blocking): item 1 (Javadoc on the grouping contract), item 3 (PR description), and optionally item 2.

Overall assessment: the v3 writer, payload-based replay protection and the new ordered publication fit together correctly, and the fix closes the data-correctness gap for v2 and v3 together. Residual risks: the documented visible prefix between groups, the key-affinity requirement for partitioned upserts, and recovery after the needed snapshot history has expired (documented). Nothing was built or executed locally. This review is based on the full diff, the call chain at this head and the reproduction reported on this head. I will move my review state once CI is green.

Better alternative: none for this PR. Longer term, a single shared "commit group" helper used by both this PR and #12937 would avoid two slightly different grouping rules for the same summary keys.

@github-actions github-actions Bot removed the approved label Oct 24, 2026
@goutamadwant

Copy link
Copy Markdown
Collaborator Author

@davidzollo @DanielLeens Could you approve this workflow to run too? changes requseted are addressed and verified locally. Pls review as well. Thanks!

@DanielLeens

Copy link
Copy Markdown
Contributor

Thanks @goutamadwant for the quick follow-up and for the fix in beeda6fac. I re-reviewed the full PR at head 311b653ab earlier today (see my last review): the High-severity stale-row blocker is resolved from the source side, and I found no remaining blocking issues in the source, tests or docs. I will move my review state to approve once Build is green on the final head.

Two things are needed on your side before that can happen:

  1. Merge conflict. GitHub now reports this PR as conflicting with dev (it is currently 5 commits behind). Please sync with the latest dev and resolve the conflicts. Since the conflict resolution will produce a new head, I will re-check the diff of that merge before approving, so please keep the resolution limited to the conflicting hunks.
  2. CI. The Build check on 311b653ab has not finished yet. As the workflow runs in your fork, it is your fork's run that decides the result, so please let it complete (or re-run it after the sync). If a lane fails, tell me which one and I will check whether it relates to this diff.

I cannot approve or trigger workflow runs from my side as a reviewer, so a maintainer with write access would need to do that if the run stays pending. The non-blocking suggestions from my review (Javadoc on the grouping contract, refreshing the PR description) can be picked up in the same push. Thanks again for the patience and the thorough tests!

Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>
Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>

@DanielLeens DanielLeens 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 again, @goutamadwant, for the quick follow-up on my last round! The head moved from 311b653 (the version I last reviewed in full) to 308f17b. I compared that range: it contains five upstream dev commits that arrived through the merge (CSV preamble, Kafka tombstone, nested row byte accounting, nullable Avro ROW layout, Zeta metrics reset), the merge commit itself, and one PR-owned commit, [Docs][Iceberg] Explain checkpoint group ordering and replay identity, which only adds Javadoc/comments in IcebergAggregatedCommitter.java and IcebergFilesCommitter.java. I re-verified the Iceberg code path and the full PR diff against the new head, and none of the merged dev changes touch the Iceberg connector. The PR also no longer conflicts with dev (MERGEABLE, 0 behind of its own merge base for the Iceberg files). This is a code-reading review only; nothing was executed locally.

What Problem Does This PR Solve?

  • User pain point: Iceberg format-version 3 tables rejected CDC/upsert writes; a recovered committable could be re-published through the ordinary commit path; and a schema change inside one checkpoint could leave stale rows because an equality delete shared the data sequence number of the rows it had to remove (v2 and v3).
  • Fix approach: a v3 DeletionVectorTaskWriter (Puffin deletion vectors for rows produced by the current writer, equality deletes for older rows); versioned payload fingerprints stored in the published snapshot and checked by both ordinary commit and restore; and ordered commit groups per writer generation.
  • One sentence: v3 CDC/upsert becomes supported, with replay protection that identifies the payload rather than trusting a reusable checkpoint number, and with correct delete ordering across schema changes.

1. Code Change Review

1.1 Core Logic Analysis

Key logic (unchanged since my last full review, now with explanatory docs):

// IcebergAggregatedCommitter: round r = r-th WriteResult of each writer (one per writer generation)
for each round:
    if (round has equality delete with recordCount > 0 && !pending.isEmpty()) { groups.add(pending); pending = new ArrayList<>(); }
    pending.addAll(round results);
// 1 group -> doCommit(results, id)  (unchanged single snapshot)
// n groups -> doCommit(group, id, g, n)  (+ seatunnel.commit-group[-count], diagnostic only)

Runtime path:

IcebergSinkWriter.applySchemaChange -> IcebergRecordWriter.applySchemaUpdate: flush() saves generation g, resetWriter() starts g+1
prepareCommit -> complete() -> [g0, g1, ...] in one IcebergCommitInfo (checkpoint state)
IcebergAggregatedCommitter.commit / restoreCommit (same path) -> orderedGroups()
   -> per group IcebergFilesCommitter.pendingResults() (fingerprints of already published results are skipped)
   -> AppendFiles or RowDelta, one snapshot per group

Correctness holds under the documented key-affinity requirement: an equality delete in round r is published after all data of earlier rounds so it removes them; later rounds without equality deletes share the sequence number and are correctly not removed; deletion vectors only reference same-generation files; replay after a crash between groups skips the already published group by payload fingerprint and publishes the rest; group ordinals are never used as proof of publication. The new Javadoc now states exactly these assumptions (result-list index = writer generation, key affinity, group keys are diagnostics), which resolves my previous Issue 1. The normal CDC/upsert streaming path reaches this logic whenever a delta write is used.

1.2 Compatibility Impact

Partially incompatible (documented). For v2 and v3 CDC/upsert jobs, a checkpoint that spans writer generations can now publish several ordered snapshots instead of one, so external readers can observe a committed prefix between groups. Single-generation and append-only checkpoints keep a single snapshot with unchanged summary keys. No option, default, public SPI or checkpoint serialization format changes (IcebergCommitInfo/WriteResult untouched); old snapshots stay readable and legacy, non-fingerprinted history is evaluated fail-closed. docs/en|zh/introduction/concepts/incompatible-changes.md and the PR checklist now describe this correctly, which resolves my previous Issue 3 (the description also mentions the #12937 overlap).

1.3 Performance / Side-Effect Analysis

Grouping is linear in the results of one checkpoint with no I/O. Extra snapshots only appear for checkpoints containing a generation change (schema change), so the commit count tracks schema-change frequency, not data volume. Each additional group performs one more in-memory history walk in pendingResults; manifest reads stay bounded as described in my earlier reviews. No new threads, retries or unbounded buffers.

1.4 Error Handling and Logging

A failure between groups propagates and replay finishes the remaining groups; doCommit validates group arguments; the partial-publication guard fails closed; nothing sensitive is logged.

  • Issue 1 (Low, hardening, carried over from my last review, still open): multiple checkpoints inside one aggregated commit are committed in list/insertion order (IcebergAggregatedCommitter.java:58-67), not by ascending checkpoint ID. I found no Zeta or Flink translation path that hands them over out of order and testNativeRebatchingRetainsCheckpointBoundaries covers the ascending case, so this is not a live bug. Best fix: sort by checkpoint ID when all IDs are positive and add a reversed-input test. Raised by another reviewer: no.
  • Issue 2 (Low, style): fully qualified names are used inside method bodies (org.mockito.InOrder at IcebergAggregatedCommitterTest.java:82,196, org.apache.iceberg.DeleteFile at IcebergSinkIT.java:196) where a normal import would do. Raised by another reviewer: no.

2. Code Quality Assessment

2.1 Coding Standards

ASF headers and Javadoc are in place; the new method-level docs and the one-line comment on the group-property constants are accurate. See Issue 2 for the remaining style nit.

2.2 Test Coverage and Test Stability

Rating: Stable.

  • preservesUpsertOrderingAcrossSchemaChange (IcebergFormatVersionTest.java, v2 and v3): real writer, INSERT, schema change, UPDATE_AFTER, real commit against a local catalog; asserts one live row with new values, two snapshots, group properties, replay through commit and restore without new snapshots, and that a different payload under a reused checkpoint ID is still published. Reverting the grouping makes it read two live rows and fail. The independent reproduction by @zhengxiang378928908-code on 311b653 (v2 and v3 now return only 1=a2) matches this.
  • recoversAfterPublishingOnlyFirstWriterGeneration (v2/v3, ordinary/restore) fails the second newRowDelta() and checks that replay publishes exactly the missing group once.
  • testWriterGenerationsRemainOrderedAcrossUnevenWorkers and testAppendOnlyWriterGenerationsKeepSingleCommit pin grouping and the single-snapshot fallback.
  • Removed checkpoint-ID-based isAlreadyCommitted* tests were replaced by stronger real-table tests of the payload-based semantics; I did not find a weakened assertion. The v3 rejection E2E became a positive v3 CDC E2E with exact rows and deletion-vector metadata. The new tests use temporary local catalogs and deterministic spies (no sleeps or fixed ports); the Flink/Zeta recovery E2Es wait on observed state.

2.3 Documentation Updates

English and Chinese Iceberg sink docs and both incompatible-changes.md files are updated consistently and match the code (ordered groups for v2/v3, visible prefix, single-snapshot cases, group numbers are not publication proof).

3. Architectural Soundness

3.1 Elegance

Long-term solution. Ordering is fixed where publication happens (the aggregated committer), replay safety stays payload-based, and nothing is added to the engine or public API.

3.2 Maintainability

Good: the "result index = writer generation" contract is now documented on the grouping method.

3.3 Extensibility

#12937 (flush_row_size) addresses ordering for memory-driven rotation with the same summary keys and committer files, so the overlap is real. The PR description says whichever lands second must reconcile with the first; this PR is self-contained and safe to land first.

3.4 Historical-Version Compatibility

Checkpoint state classes are unchanged, so in-flight checkpoints from a previous version restore and replay through the new path; pre-fingerprint snapshots keep the fail-closed legacy check.

4. Issue Summary

Number Issue Location Severity
1 Multiple checkpoints in one aggregated commit ordered by list order, not checkpoint ID (hardening, carried over) IcebergAggregatedCommitter.java:58-67 Low
2 Fully qualified names inside method bodies IcebergAggregatedCommitterTest.java:82,196, IcebergSinkIT.java:196 Low
- Previously: stale rows after schema change plus upsert (High); undocumented grouping contract; outdated PR description - Fixed

5. Merge Recommendation

Conclusion: Ready to merge

Blockers (must fix): none from source, tests or docs. My earlier CHANGES_REQUESTED was based only on the stale-row issue, which is fixed and covered, so from the source side it can be cleared.

Recommended fixes (non-blocking): Issue 1 (optional hardening) and Issue 2 (imports).

CI status for head 308f17b: the apache-side Build check is still in progress (run), so there is no result yet. The previous unit-test failure on this PR was the unrelated dev JDBC test break, and it is fixed in dev now. No further sync is needed. I will move my review state to approve once Build is green on this head; a maintainer with write access must still give the formal approval (REVIEW_REQUIRED).

Overall assessment: the v3 writer, payload-based replay protection and ordered publication fit together correctly and close a data-correctness gap for both v2 and v3. Residual risks, all documented: the visible prefix between groups, the key-affinity requirement for partitioned upserts, and recovery after the required snapshot history has expired. Better alternative: none for this PR; longer term a single shared commit-group helper for this PR and #12937 would avoid two slightly different grouping rules for the same keys.

@DanielLeens DanielLeens 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 again for the contribution, @goutamadwant. The CI fact in my previous review has changed: the Build check (run) is now SUCCESS on the unchanged head 308f17b81cbf.

In my last review CI was the only remaining gate and I had no outstanding source-level blocker, so I am changing my earlier comment-only review to an approval. The final merge still needs a write-capable committer. Thanks for the careful work!

@DanielLeens

Copy link
Copy Markdown
Contributor

Thanks for the contribution. The merge-gate fact cited in my previous conclusion has changed: GitHub now reports a real merge conflict. This follow-up corrects that gate on the unchanged reviewed head; it is not another full source review or a sync request based only on being behind.

Gate snapshot (2026-10-28T15:53:58+00:00), head 308f17b81cbf71ff6ada777d4267bad32c4d4106: Build is SUCCESS.

CI is green, but that does not remove the separate conflict gate below. My previous source conclusion and any independent source requests remain as recorded; no new effective approval or dismissal is performed by this clarification.

Separately, GitHub reports merge conflicts with dev. Please sync the latest base and resolve the conflicts first; the resulting new head needs checks and re-review. This conflict gate is independent of CI and existing source findings.

Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>

@DanielLeens DanielLeens 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.

Sorry for missing the remaining core-state comment coverage in the last review. Issue 1 below is a carryover omission, not a new regression introduced by this head. I re-reviewed the complete 24-file diff at 029ad39 and the actual writer, committer, reader and engine-adapter callers. The earlier stale-row blocker remains resolved. The previous conflict/CI guidance is superseded: this head is mergeable and Build is green, including the recovery executions detailed below.

What Problem Does This PR Solve?

User pain point: Iceberg v3 replaces position-delete files with deletion vectors, while replay and schema-driven writer rotation must preserve CDC updates without duplicating old rows.

Fix approach: Select a v3 writer that emits Puffin vectors for its own new data files and equality deletes for prior rows; publish writer generations in order and identify committed immutable payloads in snapshot summaries.

One-sentence summary and example: INSERT(1,a), a schema change, then UPDATE_AFTER(1,b) is published in ordered snapshots so only the new row remains visible after completion and replay.

1. Code Change Review

1.1 Core Logic Analysis

IcebergSinkWriter.write reaches IcebergRecordWriter and format-based writer selection. Schema updates flush the old task writer and reset it, producing chronological WriteResult entries. DeletionVectorTaskWriter remembers the file and row position of current-writer keys; replacements remove those positions with DVs, while absent keys become equality deletes. IcebergAggregatedCommitter groups by checkpoint, then result-list generation, starting a new snapshot group before later equality deletes. Both ordinary and restore commits use pendingResults, which walks the selected branch and recognizes versioned per-result fingerprints. Legacy candidates require actual file identities, never just a checkpoint number.

Before:

// v3 delta writer rejected; generation results flattened into one snapshot
// checkpoint-number-only recovery could confuse different jobs

After:

if (deltaWrite && formatVersion == 3) return deletionVectorWriter;
// publish ordered generations, recording immutable result fingerprints
// restoreCommit delegates to the same replay-safe commit path

Key Findings

  • Position capture precedes RollingDataWriter.write, which can roll after a row; the 2,100-row fixture checks this boundary.
  • Deep copies stabilize retained binary/nanosecond keys; reader-side normalization applies only when equality keys contain TIMESTAMP_NANO.
  • Fingerprints include the referenced data file and Puffin blob offset/length, distinguishing vectors sharing one physical file.
  • Duplicate results are removed, mixed published/new results retain only new units, and a partially matched legacy unit fails closed.
  • Group ordinal/count are diagnostics, not proof of publication. Append-only and single-generation checkpoints retain one snapshot.
  • No aggregate abort deletes an already-published prefix; writer abort only removes attempt-owned new files.

Complete runtime path

RowKind -> IcebergSinkWriter -> IcebergRecordWriter -> v2 writer / v3 DeletionVectorTaskWriter
 -> schema update: flush old writer, reset -> chronological WriteResult list
 -> prepareCommit(checkpointId) -> engine combine -> IcebergAggregatedCommitter
 -> checkpoint grouping -> ordered writer generations -> IcebergFilesCommitter
 -> branch/history fingerprint check -> pending files -> AppendFiles or RowDelta
 -> crash/replay through ordinary or restore commit -> skip published units
 -> source scan -> GenericDeleteFilter + normalized nanos equality keys -> residual/projection

The source resolves the independently reproduced schema-change stale-row case for both v2 and v3. Later equality deletes get a newer sequence number than preceding generations. Actual Flink 1.20 calls ordinary commit after combining recovered committables, so sharing the check with restoreCommit is necessary. Different jobs reusing checkpoint 1 are distinguished by files. Correctness remains conditional on documented key affinity, per-key order, retained evidence and fencing of overlapping attempts. No general atomic multi-snapshot checkpoint or HA-failover guarantee is inferred.

1.2 Compatibility Impact

Partially incompatible, explicitly documented. v2 remains the default and retains its writer selection, but both v2/v3 CDC checkpoints spanning generations may now expose a committed prefix across several snapshots. EN/ZH incompatible-changes documents explain this. Public options, plugin registration, distribution entries and checkpoint state classes remain unchanged. v3 moves from rejection to supported bounded CDC/upsert behavior; formats above v3 remain rejected for delta writes.

1.3 Performance / Side-Effect Analysis

The writer retains per-key positions per active partition until completion, plus deletion vectors and owned file paths; memory scales with checkpoint cardinality. Each group walks retained snapshot ancestry and computes fingerprints; legacy candidates may read manifests, with a live-file fallback only for unknown IDs and broken ancestry. This avoids a live-table scan for normal fingerprinted positive-ID commits, but history length still matters. No throughput or bounded-memory claim is made.

1.4 Error Handling and Logging

Write/close failures are sticky, subsequent complete fails, secondary cleanup errors are suppressed, and partial Puffin output is tracked for abort. Manifest-read failures and partial legacy publication propagate instead of guessing. Failure between groups leaves a visible prefix that replay completes. Fingerprint checking is not a cross-process lock; engine fencing is a documented prerequisite.

Issue 1: Complete comments for the new replay and writer-state invariants

  • Location: seatunnel-connectors-v2/connector-iceberg/src/main/java/org/apache/seatunnel/connectors/seatunnel/iceberg/sink/writer/DeletionVectorTaskWriter.java:75-79,281,289-321; sink/commit/IcebergFilesCommitter.java:110-181
  • Problem description: The new rows map, createdFiles/dvOutput/failure lifecycle and PartitionWriter.insert/delete lack explanatory comments. pendingResults has useful branch comments, but no complete method contract for retained-history scope, legacy partial matches and concurrency/fencing. The class/grouping comments do not explain every nontrivial state field and core method required by this review protocol. This was present and omitted in the previous review.
  • Potential risk: A later cleanup, rolling or replay change can violate ownership, sticky-failure or publication-proof assumptions without recognizing the invariant.
  • Best improvement: Add concise comments explaining current-writer-only key positions, why position is captured before the rolling write, DV versus equality-delete selection, attempt-owned file cleanup and sticky failure. Document pendingResults input/output, retained branch-history boundary, partial-match failure and external fencing requirement. No functional rewrite is requested.
  • Severity: Medium; blocking.
  • Raised by another reviewer: No

Issue 2: Keep the checkpoint-ordering assumption explicit

  • Location: IcebergAggregatedCommitter.java:58-67
  • Problem description: Multiple checkpoint groups preserve incoming insertion order rather than sorting positive checkpoint IDs. I did not establish an actual caller delivering reversed IDs, so this remains optional hardening rather than a demonstrated correctness defect.
  • Potential risk: A future adapter that changes ordering could publish a later checkpoint first.
  • Best improvement: Document the adapter ordering precondition or add explicit positive-ID ordering with a real-table reversed-input regression if that input is intended to be supported.
  • Severity: Low; non-blocking.
  • Raised by another reviewer: Carried forward from the earlier Daniel review; no duplicate independent finding.

Issue 3: Use normal imports in the expanded fixtures

  • Location: IcebergAggregatedCommitterTest.java:82,196; IcebergSinkIT.java:196
  • Problem description: Several method bodies use fully qualified Mockito and Iceberg types despite no name conflict.
  • Potential risk: Minor readability cost in already substantial recovery fixtures.
  • Best improvement: Use ordinary imports when touching these lines.
  • Severity: Low; non-blocking.
  • Raised by another reviewer: No

2. Code Quality Assessment

2.1 Coding Standards

The grouping Javadoc and diagnostic-summary comments are accurate, ASF headers are present and no dependency is added. Issue 1 narrows the remaining required comment work; it does not reopen the resolved grouping-contract discussion. Immutable file identities and existing state classes avoid a new serialized checkpoint protocol.

2.2 Test Coverage and Test Stability

Test stability: Stable. IcebergFormatVersionTest checks exact rows and duplicate keys, all three data formats, rolling, abort/partial-Puffin failures, schema/partition changes and ordinary/restore replay. recoversAfterPublishingOnlyFirstWriterGeneration asserts the exposed prefix and exact final rows/snapshot count. Real-table committer tests replace obsolete number-only expectations; delegate mocks supplement those tests rather than being sole correctness evidence. IcebergFlinkRecoveryIT:215-350 uses bounded state polling, verifies a different worker registration and original checkpoint restoration, and completes new checkpoints. IcebergZetaRecoveryIT:121-177 checks actual restored offsets and new rows after SIGKILL. Paths are unique and waits are bounded; the intentional polling sleeps are not blind success criteria. Spark is excluded from CDC row-kind testing with a stated capability reason. No local execution was performed.

Local analysis: fetched the head, created independent branch seatunnel-review-12881, read the complete 24-file diff and traced affected callers with git show/git grep. No local compilation, unit tests, integration tests, E2E, containers, or job execution was performed, as requested. These are source-analysis conclusions; execution evidence is the current-head CI below.

2.3 Documentation Updates

Both sink guides describe v3 writer boundaries, retained recovery evidence, partition/key-affinity requirements and visible ordered prefixes. Both incompatible-changes files correctly include v2 as affected. The PR body labels earlier local recovery evidence as historical; the current hosted evidence below is stronger for the checked matrix and should be kept distinct from HA/cloud/Spark coverage.

3. Architectural Soundness

3.1 Elegance of the Solution

Long-term solution. Publication ordering belongs in the Iceberg aggregated committer, while immutable payload verification belongs in the file committer. The connector-specific timestamp normalization avoids changing unrelated readers. The minimal remaining change is comment completion. Coordinate the overlapping grouping/summary-key implementation with #12937 when either lands, without importing an unmerged prerequisite.

3.2 Maintainability

Keep generation ordering, payload identity and cleanup ownership separate. A sequence-number or group-number shortcut would reopen the old failure. The new core state deserves the comments requested in Issue 1; no framework extraction is necessary for this PR.

3.3 Extensibility

Versioned fingerprint summaries leave an explicit evolution boundary. Existing-DV rewriting, row lineage, Variant/default values and broader v3 features remain separate slices; this PR does not establish them.

3.4 Historical-Version Compatibility

No checkpoint-state class changes were found. Legacy publication checks are preserved conservatively, but unchanged serialization alone is not proof that every old binary checkpoint restores. Evidence-expired recovery and overlapping unfenced attempts remain outside the documented guarantee.

4. Issue Summary

Number Issue Location Severity
1 Complete comments for the new replay and writer-state invariants seatunnel-connectors-v2/connector-iceberg/src/main/java/org/apache/seatunnel/connectors/seatunnel/iceberg/sink/writer/DeletionVectorTaskWriter.java:75-79,281,289-321; sink/commit/IcebergFilesCommitter.java:110-181 Medium
2 Keep the checkpoint-ordering assumption explicit IcebergAggregatedCommitter.java:58-67 Low
3 Use normal imports in the expanded fixtures IcebergAggregatedCommitterTest.java:82,196; IcebergSinkIT.java:196 Low

5. Merge Recommendation

Conclusion: Ready to merge after fixes

  1. Blockers — must be fixed
    • Issue 1: Complete comments for the new replay and writer-state invariants Complete the small comment-only requirement in Issue 1 and retain green checks on the resulting head; effective maintainer approval remains required.
  2. Recommended fixes — non-blocking
    • Issue 2: Keep the checkpoint-ordering assumption explicit; Issue 3: Use normal imports in the expanded fixtures

Overall assessment: The former data-correctness issue is resolved and current hosted recovery tests pass. The remaining source request is documentation of core invariants, explicitly acknowledged as my earlier omission.

Existing-review reconciliation: +1 to @zhengxiang378928908-code on the original stale-row counterexample and combined grouping/fingerprint approach. orderedGroups plus the v2/v3 exact-row and partial-publication replay tests resolve it; no duplicate High issue is raised. The earlier grouping-contract and incompatible-change documentation requests are addressed. @nzw921rx's historical conditional LGTM is not a current-head maintainer approval. The earlier conflict at 308f17b is no longer the current gate, and historical Daniel approval does not cover this new head. Author replies have been incorporated into this single full review.

Head 029ad3935b02345559e175b427e6a486f8c54ecf. Live base dev at 3ac2d1cec6aa7b3741c4dacdd23020f778e23c02; comparison ahead, ahead 7 / behind 0; merge state BLOCKED. Behind/diverged alone is recorded metadata, not a request to synchronize upstream. Checks: Signal pull request update = SUCCESS; Build = SUCCESS

Current Build is SUCCESS, backed by fork run 41017562419. All four unit-test matrix lanes and both Iceberg integration lanes passed. The JDK 17 Iceberg job log explicitly reports IcebergFlinkRecoveryIT 8/0/0/0, IcebergZetaRecoveryIT 2/0/0/0, IcebergSinkParallelCommitIT 11/0/0/0 and IcebergSinkIT 35/0/0/0 (tests/failures/errors/skips), followed by BUILD SUCCESS. Skipped unrelated lanes are not counted as executed coverage. No new upstream synchronization request is justified by branch age alone.

Daniel has READ repository permission. This COMMENT records the technical conclusion; it is not an effective maintainer approval or a dismissal of any historical requested-changes entry. A write-capable maintainer owns the final repository approval gate.

@DanielLeens

Copy link
Copy Markdown
Contributor

Thanks for keeping this contribution moving. I need to update the merge-gate information on unchanged head 029ad3935b02345559e175b427e6a486f8c54ecf: GitHub currently reports actual content conflicts with dev (CONFLICTING / DIRTY). This supersedes the earlier no-conflict/blocked snapshot; it is a content-conflict request, not a sync request based solely on branch age.

Please sync the latest dev and resolve the conflicts while preserving both this PR's intended change and the intervening upstream changes, then obtain required checks on the resulting head.

The existing comment requirement for the replay and writer-state invariants remains open; the previously non-blocking recommendations stay non-blocking. The preceding complete source review remains the source assessment; this is a gate clarification, not another full review or a new list of source findings.

Current existing-head checks: Build=SUCCESS. Compare diverged, ahead 7 / behind 9. Checked 2026-11-01T11:07:26.403321+00:00. Daniel has READ repository permission; final effective approval/dismissal and merge belong to a write-capable maintainer. Happy to review the resulting head once the conflict is resolved.

Signed-off-by: Goutam Adwant <workwithgoutam@gmail.com>

@DanielLeens DanielLeens 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 for the quick follow-up, @goutamadwant. I re-read the complete current diff at e9a942f8f35 (24 files, +4097/-386 against dev) together with the single commit since my last full pass (029ad393 -> e9a942f8: a comment-only change plus a merge of current dev). The conflict I reported is gone and the comment request from my last review is addressed. One carryover below is mine: I missed it in earlier rounds.

Sorry about that: Issue 1 is a carryover omission from my previous reviews of the replay-fingerprint design, not something introduced by your latest commit. I should have measured the snapshot-summary size earlier.

What Problem Does This PR Solve?

  • User pain: Iceberg format v3 replaced position-delete files with deletion vectors, so CDC/upsert into a v3 table was rejected. Separately, a schema change inside a checkpoint reset the task writer, and the new writer's equality delete shared a data sequence number with the earlier writer's data file, leaving the stale row visible (reproduced independently on v2 and v3 by @zhengxiang378928908-code).
  • Fix approach: A v3 delta writer that records the position of each key in its own new files (Puffin deletion vectors) and writes equality deletes for older rows; ordered publication groups per writer generation so later equality deletes land in a newer snapshot; and replay-safe commits that recognize already-published results by immutable payload fingerprints in the snapshot summary, shared by ordinary commit and restore.
  • One sentence: INSERT(1,a), schema change, UPDATE_AFTER(1,b) now publishes two ordered snapshots and reads back only 1=b, including after a crash between the two.

1. Code Change Review

1.1 Core Logic Analysis

Delta since my last review (029ad393 -> e9a942f8), Iceberg paths only:

// DeletionVectorTaskWriter: comments on createdFiles, dvOutput, failure, rows,
//   insert() (position captured before the rolling write) and delete() (DV vs equality delete)
// IcebergFilesCommitter.pendingResults: Javadoc on retained-history scope, legacy
//   partial-match failure and the external-fencing requirement

The rest of the changed files in the merge are dev content that is not part of this PR's diff: git diff apache/dev against this head lists only the 24 PR files, and the head is 0 commits behind dev.

Runtime path (re-checked against the code on this head):

RowKind -> IcebergSinkWriter -> IcebergRecordWriter -> IcebergWriterFactory
   v3 + delta -> DeletionVectorTaskWriter (formatVersion > 3 delta -> rejected)
   v2 / append -> existing writers
 schema change: flush old writer, reset -> chronological List<WriteResult> per writer
 prepareCommit(checkpointId) -> combine -> IcebergAggregatedCommitter
   group by checkpoint (insertion order) -> orderedGroups(): result-list index = generation
      new group when a generation has a nonempty equality delete and earlier results are pending
   -> IcebergFilesCommitter.doCommit(group) -> pendingResults(): walk branch ancestry,
      match "v1:" fingerprints, legacy candidates only by file identity -> AppendFiles / RowDelta
      with seatunnel.commit-ids (+ diagnostic group ordinal/count)
 restoreCommit == commit (Flink replays recovered committables through ordinary commit)
 source: GenericDeleteFilter + IcebergInternalRecordWrapper keys (timestamp_ns normalized)

Key findings:

  1. The default v2 path keeps its writer selection; v3 CDC/upsert now reaches the new writer. For the stale-row case the group split gives the equality delete a newer sequence number than the earlier data file, so 1=a is removed. The v3 delta writer behaves like Iceberg's own equality-delta writer: a key inserted twice keeps the later row, a delete of an unknown key becomes an equality delete, and UPDATE_BEFORE is ignored in upsert mode.
  2. The removed v3 guards (checkDeltaWriteSupported in IcebergWriterFactory and checkIdentifierFieldsSupported in SchemaUtils) are exactly the ones this feature intends to replace; nothing unrelated is deleted. Formats above v3 are still rejected for delta writes.
  3. Replay safety: fingerprints include content type, spec ID, path, format, record count, size, and for deletion vectors the referenced data file and Puffin blob offset/length. A checkpoint number alone only selects legacy candidates; a partially matched legacy payload fails rather than replaying a subset.
  4. The reader change (IcebergFileScanTaskReader) only replaces Iceberg's InternalRecordWrapper with the connector's IcebergInternalRecordWrapper (same conversions plus timestamp_ns) and routes equality-delete loading through a copy-retaining loader only when the key schema contains a timestamp_ns column; other key schemas use Iceberg's own loader.

1.2 Compatibility Impact

Partially incompatible, documented. v2 stays the default; public options, plugin registration and checkpoint state classes are unchanged. For v2 and v3 CDC/upsert jobs, a checkpoint that spans writer generations can now produce several snapshots, so external readers may observe a committed prefix between groups. Both incompatible-changes.md files (under ## dev) describe this. The new entry is a single paragraph, whereas neighboring entries use "Affected jobs / Behavior change / Migration" bullets (Issue 3).

1.3 Performance and Side Effects

Writer memory: one entry per distinct key per partition per checkpoint, the same shape as Iceberg's own equality-delta writer. Commit: each group walks in-memory snapshot ancestry; manifests are read only for legacy candidates. The new cost I had not sized is the snapshot summary (Issue 1).

1.4 Error Handling, Logging and Issues

Write and close failures are sticky and complete() fails after them; abort deletes only files created by this attempt (including a partially written Puffin stream); manifest-read errors propagate. A crash between groups leaves a visible prefix that replay completes (tested). Fingerprint checking is not a lock against two concurrent committers of the same payload; the docs state that external fencing is required.

Medium

Issue 1 (carryover, mine): seatunnel.commit-ids grows with sink parallelism and with every retained snapshot

  • Location: IcebergFilesCommitter.java commit(...) (the ids string: "v1:" plus one 64-hex SHA-256 per pending writer result, comma separated) and fileId/commitId.
  • Problem: Every writer result that has files adds a 64-character digest to the summary of the snapshot it is published in. Snapshot summaries are stored in the table metadata JSON, which Iceberg rewrites on each commit and every reader loads. Rough size: parallelism 32 gives about 2 KB of fingerprints per snapshot, comparable to or larger than the standard summary entries. At one checkpoint per minute and seven days of retained snapshots (about 10,000) that is on the order of 20 MB of extra metadata. At parallelism 100 and above it is correspondingly larger. This is my estimate from the code, not a measurement.
  • Risk: Slower commits, table loads and catalog traffic for high-parallelism, high-checkpoint-frequency CDC jobs that retain many snapshots, which are exactly the jobs this feature targets.
  • Best improvement: Keep per-result membership (it is what lets mixed published/new results be separated) but store a shorter digest (for example the first 128 bits, base64 or 32 hex characters), and add a sentence to the docs recommending routine snapshot expiration consistent with the recovery-evidence window. A single digest for the whole group would not work because replay needs per-result matching.
  • Severity: Medium, non-blocking.
  • Raised by another reviewer: No.

Low

Issue 2: Document the checkpoint-ordering precondition in IcebergAggregatedCommitter.commit (carried forward, unchanged)

  • Location: IcebergAggregatedCommitter.java:58-67.
  • Problem: Multiple checkpoints in one commit are published in incoming (insertion) order rather than sorted by positive checkpoint ID. I did not find a caller that delivers them reversed.
  • Risk: A future adapter that changes ordering could publish a later checkpoint first.
  • Best improvement: One sentence stating the precondition, or an explicit sort with a real-table regression if reversed input is meant to be supported.
  • Severity: Low, non-blocking.
  • Raised by another reviewer: No.

Issue 3: Style and doc-format nits

  • Location: IcebergFilesCommitter.java:356 (java.util.stream.Collectors.joining written fully qualified in production code while toList is already statically imported); fully qualified Mockito/Iceberg types in IcebergAggregatedCommitterTest and IcebergSinkIT (carried forward); the new incompatible-changes.md entry (EN and ZH) is one paragraph rather than the Affected jobs / Behavior change / Migration bullets used by neighboring entries.
  • Risk: Readability only.
  • Best improvement: Normal imports and the bullet structure when touching these lines.
  • Severity: Low, non-blocking.
  • Raised by another reviewer: No.

2. Code Quality

2.1 Coding Standards

The comment request from my last review is now met: the new writer state (createdFiles, dvOutput, failure, rows), insert/delete and pendingResults all explain the invariant and the why. ASF headers and the Javadoc on the grouping contract are present. No new dependency.

2.2 Tests and Stability

IcebergFormatVersionTest (exact final rows for v2/v3, all three data formats, rolling at the file-size boundary, abort and partial-Puffin failure, schema and partition changes, ordinary and restore replay), real-table IcebergFilesCommitterTest and IcebergAggregatedCommitterTest (including publishing only the first generation then replaying), IcebergFlinkRecoveryIT and IcebergZetaRecoveryIT (kill and restore with exact rows and snapshot counts, bounded Awaitility waits). No test was removed or loosened in the delta since my last review; earlier number-only expectations were replaced by file-identity expectations in prior rounds.

Flaky-stability rating: Stable. Waits are condition-based with explicit limits, paths are unique, and the recovery ITs assert state, not timing. Spark is excluded from CDC row-kind testing with a stated reason.

2.3 Documentation

English and Chinese sink guides, changelog and incompatible-changes.md are updated and consistent with the code (v3 writer behavior, partition-column and key-affinity requirements, retained recovery evidence, the visible-prefix behavior, and the manifest-scan cost for legacy payloads without a positive checkpoint ID).

3. Architectural Soundness

3.1 Fix versus Workaround

A root-cause fix: ordering belongs in the aggregated committer (it sees all writers of a checkpoint), payload identity belongs in the file committer, and the reader-side timestamp normalization stays connector-specific. Coordinate the overlapping grouping and summary keys with #12937 when either lands, as the PR description already says.

3.2 Maintainability

Generation ordering, payload identity and cleanup ownership stay separate; the new comments protect those boundaries.

3.3 Extensibility

Fingerprints are versioned (v1:), leaving room for a later format. Existing-DV rewriting, row lineage and other v3 features are explicitly separate slices.

3.4 Historical Compatibility

No checkpoint state class changes. Legacy snapshots without fingerprints are handled conservatively (file-identity match, fail on partial match). Recovery after the relevant snapshot history has expired is documented as outside the guarantee.

4. Issue Summary

Number Issue Location Severity
1 Fingerprint list in snapshot summary scales with parallelism x retained snapshots IcebergFilesCommitter.commit Medium, non-blocking
2 Checkpoint-ordering precondition undocumented IcebergAggregatedCommitter.java:58-67 Low
3 FQN in production code and tests; changelog entry format IcebergFilesCommitter.java:356, tests, docs Low

5. Merge Recommendation

Conclusion: Ready to merge

CI: on this exact head, the fork run 41352844311 was still in progress when I checked: license, dead-links, code-style, sanity and Helm checks and updated-modules-integration-test-part-1/2 are green; unit-test (4 lanes) and iceberg-connector-it (JDK 11 and 17) were still running. The previous head 029ad393 had a green Build, including the Iceberg recovery ITs, and the delta since then is comments plus a merge of dev, so I see no source-side reason for it to change, but the result for this head is not yet in. The branch is 0 commits behind dev and mergeable. Required approvals are still outstanding (review decision is REVIEW_REQUIRED).

  1. Blockers: none from source review. Remaining gates are the exact-head CI result and a write-capable maintainer approval.
  2. Recommended non-blocking: Issue 1 (shorter digest plus a docs note on snapshot expiration), then Issues 2 and 3.

Thank you for the thorough work and for coordinating with #12937.

@DanielLeens DanielLeens 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.

CI status update for head e9a942f8f3: my previous review on this head left the current-head Build result as the only open gate. The required Build check has since completed successfully on this exact head (finished 2026-11-02T03:48:28Z), so the CI fact cited in that review has changed.

CI was previously the only remaining blocker, and I found no source, test, compatibility or documentation blocker in my last full review; CI is now passing, so I am changing my conclusion to approve. Issues 1 to 3 from that review remain optional, non-blocking follow-ups.

Note that I have read-only permission on this repository, so this approval records my technical conclusion; a maintainer with write access still owns the effective approval and the merge. Thanks again for the careful work!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants