Skip to content

[api][core] Parallelize format table overwrite commit - #9397

Merged
JingsongLi merged 11 commits into
apache:masterfrom
sundapeng:upstream/format-table-cleanup-concurrency
Aug 28, 2026
Merged

[api][core] Parallelize format table overwrite commit#9397
JingsongLi merged 11 commits into
apache:masterfrom
sundapeng:upstream/format-table-cleanup-concurrency

Conversation

@sundapeng

@sundapeng sundapeng commented Aug 26, 2026

Copy link
Copy Markdown
Member

Purpose

A Format Table INSERT OVERWRITE deletes each old data file and completes each new one in its own
round trip on the driver. On a table holding thousands of files that dominates the statement: in one
profile the commit ran more than twice as long as every Spark task put together, leaving the statement
around three times slower than the writer the table had been migrated from. Deleting the old files was
the larger half of it, publishing the new ones the rest.

Both loops now run with bounded concurrency under format-table.commit.cleanup-thread-num and
format-table.commit.publish-thread-num, both defaulting to 64.

Cleanup still finishes before publication starts, and a commit that fails now removes the files it had
already published.

Tests

FormatTableCommitTest covers the bound, lazy listing, stop-and-drain on the first failure, failure
ordering, per-file rollback and every path that stays serial. ThreadPoolUtilsTest, CoreOptionsTest
and FormatTableCommitStatisticsTest cover the runner, the options and what a commit reports.

API and Format

ThreadPoolUtils gains one method, sequentialBatchedExecuteAwaitRunningTasksOnClose; its two
existing signatures are unchanged. Its shared iterator now refills as a slot frees rather than a batch
at a time, which the manifest read path gets as well. No file format change.

Documentation

The two options in docs/generated/core_configuration.html.

@sundapeng
sundapeng marked this pull request as draft August 26, 2026 06:40
@sundapeng
sundapeng force-pushed the upstream/format-table-cleanup-concurrency branch from 72e9018 to 5f10ffe Compare August 26, 2026 08:48
@sundapeng sundapeng changed the title [core] Parallelize format table overwrite cleanup [api][core] Parallelize format table overwrite cleanup Aug 26, 2026
@sundapeng sundapeng changed the title [api][core] Parallelize format table overwrite cleanup [api][core] Safely parallelize format table overwrite cleanup Aug 26, 2026
A Format Table INSERT OVERWRITE deletes the old data files it replaces one at a time on the
driver. On a table holding thousands of files that is thousands of synchronous round trips
after the last task has finished, with nothing else running.

Hand those deletes to a bounded runner. Only a catalog-managed partitioned Format Table uses
it; filesystem-discovered, unpartitioned, truncate and ordinary Paimon table paths stay
serial, and format-table.commit.cleanup-thread-num = 1 opts out.

The runner is the bounded batch execution in ThreadPoolUtils, extended here for callers that
change stored state:

Let it take an iterator, so a caller that discovers its work by listing storage does not have
to list all of it before the first task can start, and refill the window as a slot frees
rather than a batch at a time. An overwrite that replaces the whole table then holds one
partition rather than every file the table has.

Add a variant whose close waits for a task that has already started instead of interrupting
it. Interrupting a delete halfway leaves the caller unable to say whether it took effect, so a
caller that changes stored state cannot let close cancel what it has already handed out.

Give a worker its thread's classloader back, and clear the interrupt a cancelled task may
leave behind, so that neither reaches whatever the shared pool runs next.

Everything accepted is waited for before the commit fails, and failures keep their input
order.
Publishing the files a Format Table commit wrote is the other half of the same problem: one
synchronous multipart completion per file, on the driver, after cleanup has finished. Cleanup
concurrency alone leaves that untouched.

Publish through the same bounded runner, under its own
format-table.commit.publish-thread-num with the same scope and the same opt out. The caller
still publishes directly when there is one file or one thread, so a small commit gains
nothing and risks nothing.

Statistics, staging clean up and the catalog update stay on the caller, after every publish
has been waited for. Nothing reads a partition's files while another thread may still be
adding to them.

Roll a failed commit back file by file. A publication that a concurrent commit can fail
part way through leaves files behind that no partition should hold, and discarding the
staging output does not remove one that was already published. Every target belongs to this
write attempt, so removing it is safe even when a completion took effect but its response
was lost.

Carry the caller's access control context into the workers, so publication runs with the
same permissions whether or not it is handed to the pool.
@sundapeng
sundapeng force-pushed the upstream/format-table-cleanup-concurrency branch from 6d3ef33 to 29d8558 Compare August 26, 2026 18:15
@sundapeng sundapeng changed the title [api][core] Safely parallelize format table overwrite cleanup [api][core] Parallelize format table overwrite commit Aug 26, 2026
@sundapeng
sundapeng marked this pull request as ready for review August 26, 2026 18:21
…lsTest

testCloseCancelsQueuedTasksAndWaitsUninterruptibly reads runQueuedTaskOnInterrupt as soon as
workerInterrupted is released. The hook that clears the flag does so after super.interrupt(),
and it is super.interrupt() that wakes the worker and releases that latch. The read and the
clear are therefore unordered: let the closing thread lose the CPU between the two and the
assertion sees a flag the hook has not consumed yet.

That is a fixture defect, not a product one. close still cancels every unstarted task before
it interrupts a running one, and the invariants the test exists for, executions == 2 and
thirdExecuted == false, are asserted separately.

Have the hook count down a latch once it is done, and await that latch before reading the
flag. The test still kills the bug it was written for: reversing the cancel and interrupt
loops in close fails it on executions.

It reproduced on every run pinned to a single CPU and on none of the unpinned ones, which is
why a low core count runner saw it and a development machine did not.
@sundapeng
sundapeng marked this pull request as draft August 27, 2026 01:32
@sundapeng
sundapeng marked this pull request as ready for review August 27, 2026 05:40
Comment thread paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableCommit.java Outdated
Comment thread paimon-api/src/main/java/org/apache/paimon/utils/ThreadPoolUtils.java Outdated
Comment thread paimon-api/src/main/java/org/apache/paimon/utils/ThreadPoolUtils.java Outdated
@sundapeng sundapeng closed this Aug 28, 2026
@sundapeng sundapeng reopened this Aug 28, 2026
@JingsongLi

Copy link
Copy Markdown
Contributor

[P1] Preserve replacement files after overwrite cleanup has committed

In paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableCommit.java:625-643, an overwrite permanently removes the old data at lines 243-271 before publishMessages. However, preservePublishedTargetOnAbort is not set until the later metadata phase. If one parallel publish succeeds and another fails (or multipart completion succeeds but its response is lost), the catch path calls abort, and deletePublishedFile(committer.targetPath()) removes the successfully or possibly published replacement as well. The old files are already gone, so the partition/table can be left with neither the old nor the replacement data. A clean failure after all publishes has the same outcome.

Please transition overwrite messages to a state where their targets are not reclaimed after old-data cleanup succeeds and before publishing begins, while still cleaning any staging resources. Regression coverage should include partial parallel publish failure, multipart completion response loss, and post-publish clean failure under overwrite.

@sundapeng

Copy link
Copy Markdown
Member Author

Fixed in 3cbd4b9:

  • overwrite messages now transition immediately after old-data cleanup, before any replacement can be published
  • abort preserves published or possibly published targets while still cleaning staging resources; multipart cleanup failures are retained in the failure tree
  • regressions cover partial parallel publish failure, multipart completion response loss, and post-publish clean failure

Local RED-to-GREEN verification: all three new regressions failed before the fix; 76 FormatTable commit/statistics tests, 15 focused common tests, and Spotless now pass.

@sundapeng sundapeng closed this Aug 28, 2026
@sundapeng sundapeng reopened this Aug 28, 2026
@JingsongLi

Copy link
Copy Markdown
Contributor

[P2] Make preserved multipart cleanup safe to repeat

FormatTableCommit.commit already aborts these messages in its catch path (FormatTableCommit.java:372-382). FlinkFormatTableDataStreamSink.close then catches the rethrown commit failure and calls tableCommit.abort(commitMessages) again (FlinkFormatTableDataStreamSink.java:136-140). Because the new BaseMultiPartUploadCommitter.discardStaging propagates abortMultipartUpload failures, the second abort after a successful first abort (or after an already-completed upload) can return NoSuchUpload on S3/OSS/Jindo and escape from the Flink catch before the original exception is rethrown. The job still fails, but the actual publish/commit failure is replaced by a cleanup error.

Please either make absent/finished multipart staging cleanup idempotent, or catch the Flink-side abort failure and suppress it onto the original exception, and add a double-abort regression that verifies the original cause is retained.

@JingsongLi

Copy link
Copy Markdown
Contributor

Suggestion: keep the side-effect draining change focused on FormatTableCommit

The shared ThreadPoolUtils rewrite is substantially larger than the Format Table feature itself (about 218 production lines plus 588 test lines), and it also routes the existing public sequentialBatchedExecuteCloseable path used by manifest rewrite through the new state machine, TCCL/ACC propagation, failure-stop, and interrupt-normalization behavior. The only new production consumer that needs “cancel unstarted work but never interrupt started side effects, then drain before rollback” is FormatTableCommit.

Please prefer keeping the bounded submission and failure/drain logic local to FormatTableCommit, leaving the existing manifest iterator implementation untouched. If reuse is important, another reasonable option is a minimal explicit wait/drain capability on CloseableBatchIterator (or a separate implementation for the new close policy) without changing the existing scheduling and cancellation path. Normal iteration can still wait on the success path; the extra API only needs to guarantee quiescence from the failure/finally path. This would keep the PR scope and regression surface aligned with the feature.

@sundapeng

Copy link
Copy Markdown
Member Author

Fixed in 4c0e933 for the P2 double-abort report (#9397 (comment)). The Flink sink now keeps the original commit failure as the cause and attaches any repeated abort failure as suppressed, including a self-suppression guard. The regression reproduces the internal abort followed by the Flink-side abort, and verifies the exact primary/suppressed failures, two abort attempts, and writer close.

@sundapeng

Copy link
Copy Markdown
Member Author

Addressed in 4c0e933 for the scope suggestion (#9397 (comment)). The shared ThreadPoolUtils and ManifestReadThreadPool changes are restored byte-for-byte to the merge base, and the new shared executor test is removed. The bounded lazy stop/cancel/drain behavior is now private to FormatTableCommit, with focused concurrency, interruption, deterministic failure-order, and TCCL/ACC regression coverage.

@JingsongLi

Copy link
Copy Markdown
Contributor

+1

@JingsongLi
JingsongLi merged commit 7ffa180 into apache:master Aug 28, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants