github-actions[bot] commented on code in PR #66247:
URL: https://github.com/apache/doris/pull/66247#discussion_r3697830053
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTable.java:
##########
@@ -157,15 +162,18 @@ private PluginDrivenMvccSnapshot materializeLatest() {
ConnectorTableHandle handle = handleOpt.get();
// An empty (no-snapshot) connector still pins: fall back to a
snapshot id of -1.
- ConnectorMvccSnapshot connectorSnapshot =
- metadata.beginQuerySnapshot(session,
handle).orElseGet(this::emptySnapshot);
+ ConnectorMvccSnapshot connectorSnapshot = existingFence.orElseGet(
+ () -> metadata.beginQuerySnapshot(session,
handle).orElseGet(this::emptySnapshot));
// Range-view path (e.g. iceberg): thread the query's pin onto the
handle FIRST (applySnapshot), so
// the partition/freshness enumeration stays consistent with the
data-scan pin, then ask the connector
// for its range-aware view. A connector without a range view returns
empty -> fall through to the
// legacy listPartitions/LIST/timestamp path below (byte-unchanged;
the no-op applySnapshot for the
// latest pin is side-effect-free for both paimon and iceberg).
ConnectorTableHandle pinnedHandle = metadata.applySnapshot(session,
handle, connectorSnapshot);
+ // Fence hydration must list the pinned version; the ordinary latest
path keeps its legacy
+ // base-handle partition semantics for connectors whose applySnapshot
is scan-only.
+ ConnectorTableHandle partitionHandle = existingFence.isPresent() ?
pinnedHandle : handle;
Review Comment:
[P2] Hydrate initial partition accounting from the pinned handle
For an ordinary `materializeLatest()` call, `existingFence` is empty, so
this chooses the live base handle even after `beginQuerySnapshot` captured
snapshot S. Paimon later replans from the pushed predicate, so the mismatch
does not drop rows, but this live listing initializes
`selectedPartitionNum`/`totalPartitionNum`. Normal scans replace the selected
count with Paimon's actual scanned-partition count; `COUNT(*)` pushdown
deliberately keeps the Nereids count. If S+1 removes partition `p` between
snapshot capture and this listing, a COUNT(*) scan pinned to S can therefore
report fewer S+1 partitions and evade a `sql_block_rule` `partition_num` limit.
The existing positive-fence thread covers options-first hydration; this is the
initial plain/latest and preload path. Hydrate from `pinnedHandle` (or publish
no partition view when enumeration cannot honor the pin), and add a
partition-removal race that asserts COUNT(*) partition accounting and
block-rule enforcement.
##########
fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java:
##########
@@ -523,64 +628,156 @@ static Optional<Long> parseDataSizeBytes(String value) {
if (value == null || value.trim().isEmpty()) {
return Optional.empty();
}
- String normalized = value.trim().toLowerCase(Locale.ROOT).replace("_",
"").replace(" ", "");
- int unitStart = 0;
- while (unitStart < normalized.length()
- && (Character.isDigit(normalized.charAt(unitStart)) ||
normalized.charAt(unitStart) == '.')) {
- unitStart++;
- }
- if (unitStart == 0) {
- return Optional.empty();
- }
try {
- double number = Double.parseDouble(normalized.substring(0,
unitStart));
- String unit = normalized.substring(unitStart);
- long multiplier;
- switch (unit) {
- case "":
- case "b":
- case "byte":
- case "bytes":
- multiplier = 1L;
- break;
- case "k":
- case "kb":
- case "kib":
- multiplier = 1024L;
- break;
- case "m":
- case "mb":
- case "mib":
- multiplier = 1024L * 1024L;
- break;
- case "g":
- case "gb":
- case "gib":
- multiplier = 1024L * 1024L * 1024L;
- break;
- case "t":
- case "tb":
- case "tib":
- multiplier = 1024L * 1024L * 1024L * 1024L;
- break;
- default:
- return Optional.empty();
- }
- return Optional.of((long) (number * multiplier));
- } catch (NumberFormatException e) {
+ // Keep the BE guard's accepted grammar identical to the Paimon
option parser that will
+ // consume this value; accepting a superset lets invalid
serialized options reach scans.
+ return Optional.of(MemorySize.parse(value).getBytes());
+ } catch (IllegalArgumentException e) {
return Optional.empty();
}
}
private void initTable() {
Preconditions.checkState(params.containsKey("serialized_table"));
table = PaimonUtils.deserialize(params.get("serialized_table"));
+ String encodedSystemSource = params.get(PAIMON_OPTION_PREFIX +
DORIS_SERIALIZED_SYSTEM_SOURCE);
+ FileStoreTable systemSource = encodedSystemSource == null
+ ? null : PaimonUtils.deserialize(encodedSystemSource);
+ table = applyBackendManifestParallelism(table,
+ params.get(PAIMON_OPTION_PREFIX +
DORIS_MANIFEST_PARALLELISM_CAP),
+ Runtime.getRuntime().availableProcessors(), systemSource,
+ params.get(PAIMON_OPTION_PREFIX + DORIS_SYSTEM_TABLE_TYPE));
+ validateSerializedReaderOptions(table);
paimonAllFieldNames = PaimonUtils.getFieldNames(this.table.rowType());
if (LOG.isDebugEnabled()) {
LOG.debug("paimonAllFieldNames:{}", paimonAllFieldNames);
}
}
+ static Table applyBackendManifestParallelism(
+ Table table, String feParallelismCap, int localCapacity) {
+ return applyBackendManifestParallelism(
+ table, feParallelismCap, localCapacity, null, null);
+ }
+
+ static Table applyBackendManifestParallelism(
+ Table table, String feParallelismCap, int localCapacity,
+ FileStoreTable systemSource, String systemTableType) {
+ Table planningTable = systemSource == null ? table : systemSource;
+ List<Integer> configuredValues = new ArrayList<>();
+ collectManifestParallelism(planningTable, configuredValues);
+ // Old FEs do not send a cap, so the BE must still preserve the
hardware-independent
+ // ceiling that prevents one scan from growing Paimon's JVM-global
executor beyond 256.
+ int requestedBound = Math.min(localCapacity, MAX_MANIFEST_PARALLELISM);
+ if (feParallelismCap != null) {
+ requestedBound =
Math.min(parsePositiveManifestParallelism(feParallelismCap), requestedBound);
+ }
+ final int safeBound = requestedBound;
+ // The FE cap is a requested bound, not proof that every serialized
wrapper carries it;
+ // a later table rebuild can expose the original physical value to
this BE.
+ if (configuredValues.isEmpty()) {
+ if (systemSource == null && !(table instanceof FileStoreTable)) {
+ // Legacy FEs serialize only the system wrapper, whose public
options hide the
+ // source planner. Wrapper copy is the only compatible way to
enforce the BE cap.
+ return table.copy(Collections.singletonMap(
+ CoreOptions.SCAN_MANIFEST_PARALLELISM.key(),
String.valueOf(safeBound)));
+ }
+ return table;
+ }
+ if (configuredValues.stream().noneMatch(value -> value > safeBound)) {
+ return table;
+ }
+ int safeParallelism = Math.min(
+
configuredValues.stream().mapToInt(Integer::intValue).min().getAsInt(),
+ safeBound);
+ Map<String, String> cap = Collections.singletonMap(
+ CoreOptions.SCAN_MANIFEST_PARALLELISM.key(),
String.valueOf(safeParallelism));
+ // File-store copies must retain the FE-selected schema while only
lowering an execution
+ // bound; ordinary copy can re-resolve time travel and undo schema
pinning.
+ if (systemSource != null && systemTableType != null) {
+ FileStoreTable cappedSource =
systemSource.copyWithoutTimeTravel(cap);
+ // Read-only wrappers hide the data table's option map. Rebuild
from the transported
+ // exact source so a smaller BE can lower that hidden planner
without rewinding schema.
+ Table rebuilt = SystemTableLoader.load(systemTableType,
cappedSource);
+ if (rebuilt == null) {
+ throw new IllegalArgumentException(
+ "Unsupported Paimon system table '" + systemTableType
+ "'");
+ }
+ return rebuilt;
+ }
+ return table instanceof FileStoreTable
+ ? ((FileStoreTable) table).copyWithoutTimeTravel(cap) :
table.copy(cap);
+ }
+
+ private static int parsePositiveManifestParallelism(String value) {
+ try {
+ int parsed = Integer.parseInt(value);
+ if (parsed < 1) {
+ throw new IllegalArgumentException("Paimon manifest
parallelism cap must be positive.");
+ }
+ return parsed;
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Paimon manifest parallelism
cap must be an integer.", e);
+ }
+ }
+
+ private static void collectManifestParallelism(Table table, List<Integer>
values) {
+ String configured =
table.options().get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key());
+ if (configured != null) {
+ values.add(parsePositiveManifestParallelism(configured));
+ }
+ if (table instanceof FallbackReadFileStoreTable) {
+ collectManifestParallelism(((FallbackReadFileStoreTable)
table).fallback(), values);
+ }
+ if (table instanceof DelegatedFileStoreTable) {
+ collectManifestParallelism(((DelegatedFileStoreTable)
table).wrapped(), values);
+ }
+ }
+
+ private static void validateSerializedReaderOptions(Table table) {
Review Comment:
[P1] Backstop the newly bounded split target on the BE
The FE now rejects `source.split.target-size < 1` because zero defeats
Paimon's bin packing and expands planning to one split per data file, but this
rolling-upgrade guard checks only batch size and async threshold. An older FE
can still serialize `source.split.target-size=0 B`; for a deferred `$files`
reader, planning happens after deserialization and the zero value reaches
`newReadBuilder()` unchanged. The existing old-FE threads cover async bounds
and hidden batch readers, not this newly allowlisted option. Mirror the
positive split-target check here across the deserialized/hidden system source
and add an old-FE deferred `$files` fixture with a zero target.
##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonConnectorMetadata.java:
##########
@@ -731,8 +741,17 @@ public Optional<ConnectorMvccSnapshot> resolveTimeTravel(
// must not be re-evaluated later, or split planning would
read a different version than
// the one whose schema was bound. Resolution runs against the
LATEST table, because the
// options themselves are what selects the version.
- Map<String, String> resolved =
- PaimonScanParams.resolveOptions(table,
spec.getOptions());
+ boolean usesStatementFence =
spec.getLatestSnapshotFence().isPresent()
+ &&
PaimonScanParams.usesStatementSnapshot(spec.getOptions());
+ Map<String, String> resolved;
+ if (usesStatementFence) {
+ // Planning-only aliases own different table projections,
not different
+ // versions. Reuse the statement fence even if latest
advances between binds.
+ resolved = PaimonScanParams.pinOptionsToSnapshot(
Review Comment:
[P1] Resolve the fallback pin before dropping its catalog loader
This statement-fence path turns selector-free tuning such as
`t$ro@options('read.batch-size'='4096')` into `scan.snapshot-id=S`. Later,
`tableForBackend` removes both branch catalog loaders and `reapplyScanParams`
copies that pin onto the catalog-less fallback pair; Paimon's fallback-id
rewrite then searches the newest snapshot files rather than the version-managed
fallback catalog pointer. After an unpublished commit or rollback, `$ro` can
therefore return fallback rows from a generation the catalog does not expose.
This is distinct from the decorator thread: the fallback pair is retained here,
but it selects the wrong generation. Resolve both branches' catalog-visible
pins while their loaders are still present (or avoid replaying this synthetic
id on the catalog-less pair), and cover reader-only OPTIONS on a
version-managed fallback `$ro` with a newer fallback snapshot file beyond the
catalog pointer.
--
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]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]