Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
902281c
[core] Parallelize per-partition file listing for catalog-managed for…
XiaoHongbo-Hope Jul 24, 2026
616f04b
[core] Drop the PartitionSplitPlanner abstraction
XiaoHongbo-Hope Jul 24, 2026
4a826a0
[core] Remove redundant comments in FormatTableScan split planning
XiaoHongbo-Hope Jul 24, 2026
d7e9235
[core] Default format-table list parallelism to 64; filter partitions…
XiaoHongbo-Hope Jul 24, 2026
38a7f5e
[core] Use a dedicated format-table list pool (max 1000, 64 per scan)…
XiaoHongbo-Hope Jul 24, 2026
fcc41e0
[core] Drop the missing-partition warn comment in FormatTableScan
XiaoHongbo-Hope Jul 24, 2026
cc06a64
[docs] Regenerate core config docs for format-table.scan.list-paralle…
XiaoHongbo-Hope Jul 25, 2026
ae90b04
[core] Clamp format-table list parallelism to at least 1
XiaoHongbo-Hope Jul 25, 2026
a3f3d07
[core] Refactor format table split enumeration
XiaoHongbo-Hope Jul 25, 2026
ba68029
[core] Split format table enumerator into catalog and filesystem impls
XiaoHongbo-Hope Jul 25, 2026
f06e3fb
[core] Reuse the shared manifest-read pool for format table listing
XiaoHongbo-Hope Jul 25, 2026
103010b
[core] Bound the format table listing pool and preserve the caller's …
XiaoHongbo-Hope Jul 25, 2026
45fd951
[test] Guard that format table listing establishes the filesystem on …
XiaoHongbo-Hope Jul 25, 2026
7fa68f3
[core] Restore Trino-style listing pool: max 1000, 64 per scan, reusi…
XiaoHongbo-Hope Jul 25, 2026
d20335c
[core] Name the enumerator SplitEnumerator with Catalog/FileSystem im…
XiaoHongbo-Hope Jul 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/generated/core_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,12 @@
<td>Boolean</td>
<td>Format table file path only contain partition value.</td>
</tr>
<tr>
<td><h5>format-table.scan.list-parallelism</h5></td>
<td style="word-wrap: break-word;">64</td>
<td>Integer</td>
<td>The parallelism of listing partition files during split planning for a Format Table with catalog-managed partitions.</td>
</tr>
<tr>
<td><h5>full-compaction.delta-commits</h5></td>
<td style="word-wrap: break-word;">(none)</td>
Expand Down
12 changes: 12 additions & 0 deletions paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,14 @@ public InlineElement getDescription() {
+ "Note: Scale-up this parameter will increase memory usage while scanning manifest files. "
+ "We can consider downsize it when we encounter an out of memory exception while scanning");

public static final ConfigOption<Integer> FORMAT_TABLE_SCAN_LIST_PARALLELISM =
key("format-table.scan.list-parallelism")
.intType()
.defaultValue(64)
.withDescription(
"The parallelism of listing partition files during split planning for "
+ "a Format Table with catalog-managed partitions.");

public static final ConfigOption<Duration> STREAMING_READ_SNAPSHOT_DELAY =
key("streaming.read.snapshot.delay")
.durationType()
Expand Down Expand Up @@ -3708,6 +3716,10 @@ public Integer scanManifestParallelism() {
return options.get(SCAN_MANIFEST_PARALLELISM);
}

public Integer formatTableScanListParallelism() {
return options.get(FORMAT_TABLE_SCAN_LIST_PARALLELISM);
}

public Integer scanBucket() {
return options.get(SCAN_BUCKET);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.paimon.table.format;

import org.apache.paimon.CoreOptions;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.FileStatus;
import org.apache.paimon.fs.Path;
import org.apache.paimon.manifest.PartitionEntry;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.table.FormatTable;
import org.apache.paimon.table.source.Split;
import org.apache.paimon.utils.Pair;
import org.apache.paimon.utils.PartitionPathUtils;
import org.apache.paimon.utils.SemaphoredDelegatingExecutor;
import org.apache.paimon.utils.ThreadPoolUtils;
import org.apache.paimon.utils.ThreadUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/** A {@link SplitEnumerator} whose partitions are managed by the catalog. */
final class CatalogSplitEnumerator extends SplitEnumerator {

private static final Logger LOG = LoggerFactory.getLogger(CatalogSplitEnumerator.class);

private static final int LIST_POOL_MAX_THREADS = 1000;

// Cached pool bounded at 1000 threads that reuses idle workers; CallerRunsPolicy lists on the
// caller for back pressure once the cap is hit.
private static final ThreadPoolExecutor LIST_POOL =
new ThreadPoolExecutor(
0,
LIST_POOL_MAX_THREADS,
1,
TimeUnit.MINUTES,
new SynchronousQueue<>(),
ThreadUtils.newDaemonThreadFactory("FORMAT-TABLE-LIST-THREAD-POOL"),
new ThreadPoolExecutor.CallerRunsPolicy());

private final FormatTablePartitionManager partitionManager;

CatalogSplitEnumerator(
FormatTable table,
CoreOptions coreOptions,
FormatTablePartitionManager partitionManager) {
super(table, coreOptions);
this.partitionManager = partitionManager;
}

@Override
List<Split> enumeratePartitions(@Nullable PartitionPredicate partitionFilter)
throws IOException {
List<Pair<LinkedHashMap<String, String>, Path>> partitions =
findPartitions(partitionFilter);
List<Split> splits = new ArrayList<>();
if (partitions.isEmpty()) {
return splits;
}

FileIO fileIO = table.fileIO();
// Establish the filesystem on the caller thread so listing workers reuse it under the
// caller's security context instead of creating it lazily under a shared worker.
fileIO.exists(new Path(table.location()));
Function<Pair<LinkedHashMap<String, String>, Path>, List<Split>> lister =
pair -> {
BinaryRow partitionRow = toPartitionRow(pair.getKey());
if (partitionFilter != null && !partitionFilter.test(partitionRow)) {
return Collections.emptyList();
}
try {
return createSplits(fileIO, pair.getValue(), partitionRow);
} catch (FileNotFoundException e) {
warnMissingPartition(pair.getKey(), pair.getValue());
return Collections.emptyList();
} catch (IOException e) {
throw new RuntimeException(
"Failed to list files for partition " + pair.getValue(), e);
}
};
int parallelism =
Math.min(
LIST_POOL_MAX_THREADS,
Math.max(1, coreOptions.formatTableScanListParallelism()));
ExecutorService executor = new SemaphoredDelegatingExecutor(LIST_POOL, parallelism, false);
ThreadPoolUtils.randomlyExecuteSequentialReturn(executor, lister, partitions)
.forEachRemaining(splits::add);
return splits;
}

@Override
List<Pair<LinkedHashMap<String, String>, Path>> findPartitions(
@Nullable PartitionPredicate partitionFilter) {
Optional<Predicate> extracted = FormatTableScan.extractPartitionPredicate(partitionFilter);
Map<String, String> prefix = leadingEqualityPrefix(extracted);
Predicate catalogFilter = extracted.orElse(null);
List<Partition> partitions = partitionManager.listPartitions(prefix, catalogFilter);
if (partitions.isEmpty() && prefix.isEmpty() && catalogFilter == null) {
warnIfFilesystemPartitionsExist();
}
return toSpecsAndPaths(partitions, coreOptions.formatTablePartitionOnlyValueInPath());
}

@Override
List<PartitionEntry> listPartitionEntries() {
List<Partition> partitions = partitionManager.listPartitions(Collections.emptyMap(), null);
if (partitions.isEmpty()) {
warnIfFilesystemPartitionsExist();
}
boolean onlyValueInPath = coreOptions.formatTablePartitionOnlyValueInPath();
List<PartitionEntry> entries = new ArrayList<>(partitions.size());
Set<Map<String, String>> seen = new HashSet<>(partitions.size());
for (Partition partition : partitions) {
if (!seen.add(partition.spec())) {
continue;
}
entries.add(
new PartitionEntry(
toPartitionRow(normalizeSpec(partition.spec(), onlyValueInPath)),
partition.recordCount(),
partition.fileSizeInBytes(),
partition.fileCount(),
partition.lastFileCreationTime(),
partition.totalBuckets()));
}
return entries;
}

private List<Pair<LinkedHashMap<String, String>, Path>> toSpecsAndPaths(
List<Partition> partitions, boolean onlyValueInPath) {
List<Pair<LinkedHashMap<String, String>, Path>> result = new ArrayList<>(partitions.size());
Path tablePath = new Path(table.location());
// A duplicate catalog entry must not duplicate all records in that partition.
Set<String> seenPartitionPaths = new HashSet<>(partitions.size());
for (Partition partition : partitions) {
LinkedHashMap<String, String> spec = normalizeSpec(partition.spec(), onlyValueInPath);
String partitionPath =
PartitionPathUtils.generatePartitionPathUtil(spec, onlyValueInPath);
if (seenPartitionPaths.add(partitionPath)) {
result.add(Pair.of(spec, new Path(tablePath, partitionPath)));
}
}
return result;
}

LinkedHashMap<String, String> normalizeSpec(
@Nullable Map<String, String> spec, boolean onlyValueInPath) {
List<String> partitionKeys = table.partitionKeys();
if (spec == null
|| spec.size() != partitionKeys.size()
|| !spec.keySet().containsAll(partitionKeys)) {
throw corruptPartitionSpec(spec);
}
LinkedHashMap<String, String> normalized = new LinkedHashMap<>();
for (String partitionKey : partitionKeys) {
String value = spec.get(partitionKey);
// In a value-only layout, "." and ".." would resolve outside the table.
try {
PartitionPathUtils.validatePartitionValueForPath(value, onlyValueInPath);
} catch (IllegalArgumentException e) {
throw corruptPartitionSpec(spec);
}
normalized.put(partitionKey, value);
}
return normalized;
}

void warnIfFilesystemPartitionsExist() {
try {
for (FileStatus status : table.fileIO().listStatus(new Path(table.location()))) {
if (status.isDir() && !status.getPath().getName().startsWith(".")) {
LOG.warn(
"Format table {} has no partitions registered in the catalog "
+ "but its location {} contains directories. Data written "
+ "before enabling catalog-managed partitions (or by clients "
+ "that do not register partitions) is invisible until the "
+ "partition metadata is synced, e.g. with MSCK REPAIR TABLE.",
table.fullName(),
table.location());
return;
}
}
} catch (IOException ignored) {
// Best-effort hint only; never fail or slow down the scan because of it.
}
}

private Map<String, String> leadingEqualityPrefix(Optional<Predicate> predicate) {
if (!predicate.isPresent()) {
return Collections.emptyMap();
}
return FormatTableScan.extractLeadingEqualityPartitionSpecWhenOnlyAnd(
table.partitionKeys(), predicate.get(), table.partitionType());
}

private IllegalStateException corruptPartitionSpec(@Nullable Map<String, String> spec) {
return new IllegalStateException(
String.format(
"Catalog returned corrupt partition metadata %s for format table %s; "
+ "expected exactly the partition keys %s with values usable as "
+ "path components.",
spec, table.fullName(), table.partitionKeys()));
}

private void warnMissingPartition(LinkedHashMap<String, String> spec, Path path) {
LOG.warn(
"Partition '{}' of format table {} is registered in the catalog but its directory "
+ "'{}' does not exist; treating the partition as empty. If the directory "
+ "was removed on purpose, drop the partition or repair the metadata, e.g. "
+ "with MSCK REPAIR TABLE.",
PartitionPathUtils.generatePartitionName(spec, false),
table.fullName(),
path);
}
}
Loading
Loading