Skip to content

[core] Parallelize per-partition file listing for catalog-managed format tables#8845

Open
XiaoHongbo-Hope wants to merge 15 commits into
apache:masterfrom
XiaoHongbo-Hope:format_table_parallel_scan
Open

[core] Parallelize per-partition file listing for catalog-managed format tables#8845
XiaoHongbo-Hope wants to merge 15 commits into
apache:masterfrom
XiaoHongbo-Hope:format_table_parallel_scan

Conversation

@XiaoHongbo-Hope

@XiaoHongbo-Hope XiaoHongbo-Hope commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

No description provided.

…mat tables

Format table split planning lists every partition's files serially, which is slow
for internal (catalog-managed) tables with many partitions (thousands of object
store LIST calls on the coordinator).

Extract the per-partition listing into a PartitionSplitPlanner strategy with two
implementations selected by the existing internal/external discriminator
(partitionManager != null):
- SequentialSplitPlanner (external): unchanged serial path; a missing partition
  directory is rethrown.
- ParallelSplitPlanner (internal): bounded, order-preserving fan-out via
  ManifestReadThreadPool; a registered partition without a directory reads as
  empty (matching Hive), any other listing failure fails the whole scan.

Split order and per-partition sort/bin-packing are unchanged, so output is
identical to the serial path. Parallelism is bounded by a new option
format-table.scan.list-parallelism (default #CPU); external tables are untouched.
}

/** Lists partition files and builds splits; per-partition sort and bin-packing are shared. */
private interface PartitionSplitPlanner {

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.

better to remove this interface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

better to remove this interface

Removed

Inline the internal/external branch directly in splits(): a private
listPartitionFilesInParallel(...) for internal (catalog-managed) tables and the
serial loop for external tables. Behavior is unchanged.
@XiaoHongbo-Hope
XiaoHongbo-Hope marked this pull request as ready for review July 25, 2026 01:34
A non-positive format-table.scan.list-parallelism would create a
SemaphoredDelegatingExecutor with zero/negative permits, making the
listing acquire() block forever. Clamp to a minimum of 1 so a
misconfigured value degrades to serial listing instead of hanging.
BinaryRow partitionRow = toPartitionRow(partitionSpec);
if (partitionFilter == null || partitionFilter.test(partitionRow)) {
try {
if (partitionManager != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we could refactor the code here; the partitionManager should be renamed to SplitEnumerator so that it directly generates the corresponding Split.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we could refactor the code here; the partitionManager should be renamed to SplitEnumerator so that it directly generates the corresponding Split.

Done

Move format-table partition discovery and split generation into a package-private FormatTableSplitEnumerator. Keep FormatTablePartitionManager focused on catalog metadata, parallelize catalog partition file listing, and preserve serial filesystem discovery.
import static org.apache.paimon.utils.PartitionPathUtils.searchPartSpecAndPaths;

/** Enumerates {@link FormatDataSplit}s for a {@link FormatTable}. */
final class FormatTableSplitEnumerator {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I mean to create a SplitEnumerator, and to two SplitEnumerators for catalog and filesystem. This can reduce many if else.

Per review, replace the partitionManager-based branching inside the
single FormatTableSplitEnumerator with two implementations chosen once
at construction by FormatTableSplitEnumerator.create:
CatalogFormatTableSplitEnumerator (catalog-managed partitions, parallel
listing) and FileSystemFormatTableSplitEnumerator (filesystem discovery,
serial). enumerate/findPartitions/listPartitionEntries are now
polymorphic, so the per-call 'partitionManager != null' if/else is gone.
The dedicated createCachedThreadPool(1000) sets corePoolSize ==
maximumPoolSize == 1000, so it spawns a new thread for every submitted
task until the pool holds 1000 threads rather than reusing idle workers,
even though per-scan concurrency is bounded by the semaphore. Listing
~1000 partitions on the coordinator could thus create ~1000 threads and
risk 'unable to create native thread'/OOM.

Delegate to ManifestReadThreadPool.randomlyExecuteSequentialReturn, the
shared IO pool Paimon already uses for scans (sized to availableProcessors,
grown only when a larger parallelism is explicitly requested and capped by
the format-table.scan.list-parallelism permit).
…filesystem context

Address review of the parallel listing:
- Size the dedicated pool at 128 (was 1000) and clamp
  format-table.scan.list-parallelism to [1, 128], so the pool reuses
  workers and can no longer grow to ~1000 threads or be pushed there by
  a large config value. Reverts the previous fix that had mutated the
  shared ManifestReadThreadPool.
- Establish the filesystem on the caller thread (fileIO.exists on the
  table location) before dispatching, so the listing workers reuse a
  filesystem created under the caller's security context instead of
  lazily creating and caching it under a shared worker's context. This
  matches the FileStore scan path, which reads the snapshot on the
  caller thread before its parallel manifest reads.
…ng workers

128 was an arbitrary cap; revert to the intended model - per-scan
concurrency 64 (format-table.scan.list-parallelism) over a shared pool
capped at 1000 (Trino's hive.split-loader-concurrency /
hive.max-split-iterator-threads). Build the pool as a bounded cached
pool (core 0, SynchronousQueue) so it reuses idle workers and grows only
on demand, instead of createCachedThreadPool whose corePoolSize == max
spawns a new thread per task up to the cap. CallerRunsPolicy applies back
pressure once the cap is hit.
…pls per review

Rename FormatTableSplitEnumerator -> SplitEnumerator,
CatalogFormatTableSplitEnumerator -> CatalogSplitEnumerator, and
FileSystemFormatTableSplitEnumerator -> FileSystemSplitEnumerator.
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.

3 participants