zhuxiangyi opened a new pull request, #9667:
URL: https://github.com/apache/paimon/pull/9667

   ### Purpose
   
   Closes #9666.
   
   Structured Streaming delivers exactly-once only if the sink is idempotent 
for a repeated batch id.
   When a query fails after the sink returns from `addBatch` but before Spark 
records the batch as
   completed, the restarted query replays that micro-batch with its original 
batch id.
   
   `PaimonSink` received the batch id but used it only to pace full compaction, 
and committed through
   `table.newBatchWriteBuilder()`, whose commit user is a fresh random UUID per 
builder and whose
   commit identifier is always `BatchWriteBuilder.COMMIT_IDENTIFIER = 
Long.MAX_VALUE`. Neither of the
   dimensions Paimon deduplicates on could identify a replay, so the batch was 
committed a second
   time: every row duplicated in an append-only table, and silently wrong 
values in an `aggregation`
   merge-engine table (writing `(1, 10)` and replaying that batch yields `v = 
20`).
   
   The machinery already exists in core and is what the Flink sink uses; the 
Spark sink simply took
   the batch write path.
   
   ### Tests
   
   `PaimonSinkIdempotencyTest` (new, 7 cases), each asserting the correct 
behaviour so that it fails
   without the fix:
   
   - a replayed micro-batch of an append-only table, driven end to end by 
deleting the commit log
     entry of the batch, which is exactly the checkpoint state a driver failure 
leaves behind;
   - the same when only the query id is available, i.e. the checkpoint location 
never reaches the sink
     options because it comes from `spark.sql.streaming.checkpointLocation`;
   - a replay of a batch that is not the first one of the query;
   - a replay in `complete` output mode;
   - `write.stream.commit-user` as an option of the writer and as a 
`spark.paimon.` session conf;
   - `addBatch` called twice with the same batch id through the API directly.
   
   Two of them assert the prefix of the commit user recorded in the snapshot, 
so that the case which
   is meant to exercise the query id derivation cannot pass through the 
checkpoint derivation.
   
   The full set of Spark streaming suites was run on the spark3 and spark4 
profiles (Spark 3.2, 3.4,
   3.5, 4.1): 34 suites, 349 tests.
   
   ### API and Format
   
   `BatchWriteBuilderImpl` gains `withCommitUser`, and its `newCommit()` return 
type is narrowed from
   `BatchTableCommit` to `InnerTableCommit`. The narrowing keeps the caller in 
the connector free of a
   downcast that could only fail at runtime; it is source compatible, and 
`BatchWriteBuilderImpl` has
   no subclasses.
   
   `InnerTableCommit` gains `checkFilesExistence(boolean)`. `filterAndCommit` 
verified that every file
   it is about to commit still exists, which guards a committable restored from 
an engine's state that
   may reference files deleted long ago. A caller filtering a committable it 
has just produced knows
   those files exist, so the Spark sink turns the check off; otherwise every 
micro-batch would pay a
   file listing proportional to the number of files it wrote. The default is 
unchanged, so Flink keeps
   the check.
   
   ### Documentation
   
   `docs/docs/spark/structured-streaming.md` gains an "Exactly-once" section 
covering how the commit
   user is derived, the new `write.stream.commit-user` option, and the limits: 
starting from a new
   checkpoint location gives a query a new commit user, the data files of a 
skipped replay are left to
   orphan file cleaning, and a postpone bucket table committing through the 
staged committer cannot
   deduplicate and logs a warning per micro-batch. The generated option 
reference is regenerated.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to