github-actions[bot] commented on code in PR #67186:
URL: https://github.com/apache/doris/pull/67186#discussion_r3921408053
##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVRefreshContext.java:
##########
@@ -59,12 +77,127 @@ public Map<BaseTableInfo, MTMVSnapshotIf>
getBaseTableSnapshotCache() {
return baseTableSnapshotCache;
}
+ /** Loads the union of mapped base partitions once per related table. */
+ public PreparedPartitionSnapshots preparePartitionSnapshots(Set<String>
mtmvPartitionNames)
+ throws AnalysisException {
+ return preparePartitionSnapshots(mtmvPartitionNames, false);
+ }
+
+ /** Loads only mappings whose persisted partition-name set still matches
and needs version comparison. */
+ public PreparedPartitionSnapshots
prepareComparablePartitionSnapshots(Set<String> mtmvPartitionNames)
+ throws AnalysisException {
+ return preparePartitionSnapshots(mtmvPartitionNames, true);
+ }
+
+ private PreparedPartitionSnapshots preparePartitionSnapshots(
+ Set<String> mtmvPartitionNames, boolean comparableOnly)
+ throws AnalysisException {
+ Map<MTMVRelatedTableIf, Set<String>> namesByTable = new
LinkedHashMap<>();
+ Map<MTMVRelatedTableIf, BaseTableInfo> tableInfos = comparableOnly
+ ? new LinkedHashMap<>() : Collections.emptyMap();
+ for (String mtmvPartitionName : mtmvPartitionNames) {
+ for (Map.Entry<MTMVRelatedTableIf, Set<String>> entry
+ : getByPartitionName(mtmvPartitionName).entrySet()) {
+ if (!entry.getKey().needAutoRefresh()) {
+ continue;
+ }
+ if (comparableOnly) {
+ BaseTableInfo tableInfo =
tableInfos.computeIfAbsent(entry.getKey(), BaseTableInfo::new);
+ if (!Objects.equals(entry.getValue(),
mtmv.getRefreshSnapshot()
+ .getPctSnapshots(mtmvPartitionName, tableInfo))) {
+ continue;
+ }
+ }
+ namesByTable.computeIfAbsent(entry.getKey(), ignored -> new
LinkedHashSet<>())
+ .addAll(entry.getValue());
+ }
+ }
+ for (Map.Entry<MTMVRelatedTableIf, Set<String>> entry :
namesByTable.entrySet()) {
+ loadSnapshots(entry.getKey(), entry.getValue());
+ }
+ return new PreparedPartitionSnapshots(this);
+ }
+
+ private void loadSnapshots(MTMVRelatedTableIf table, Set<String>
partitionNames) throws AnalysisException {
+ Map<String, MTMVSnapshotIf> cached =
partitionSnapshotCache.computeIfAbsent(
+ table, ignored -> new LinkedHashMap<>());
+ Set<String> knownMissing =
missingPartitionSnapshotCache.computeIfAbsent(
+ table, ignored -> new LinkedHashSet<>());
+ Set<String> missing = new LinkedHashSet<>(partitionNames);
+ missing.removeAll(cached.keySet());
+ missing.removeAll(knownMissing);
+ if (missing.isEmpty()) {
+ return;
+ }
+ Map<String, MTMVSnapshotIf> loaded = table.getPartitionSnapshots(
+ missing, this, resolveSnapshot(table));
+ if (loaded == null || loaded.containsKey(null) ||
loaded.containsValue(null)
+ || !missing.containsAll(loaded.keySet())) {
+ throw new AnalysisException("Invalid partition snapshot result for
table " + table.getName()
+ + ": requestedCount=" + missing.size() + ", returnedCount="
+ + (loaded == null ? "null" : loaded.size()));
+ }
+ cached.putAll(loaded);
+ missing.removeAll(loaded.keySet());
+ knownMissing.addAll(missing);
+ }
+
+ void recordPartitionSnapshotFailure(
+ MTMVRelatedTableIf table, String partitionName, AnalysisException
failure) {
+ partitionSnapshotFailureCache.computeIfAbsent(table, ignored -> new
LinkedHashMap<>())
+ .put(partitionName, failure);
+ }
+
public static MTMVRefreshContext buildContext(MTMV mtmv, Map<List<String>,
Set<String>> queryUsedPartitions)
throws AnalysisException {
- MTMVRefreshContext context = new MTMVRefreshContext(mtmv);
- context.partitionMappings =
mtmv.calculatePartitionMappings(queryUsedPartitions);
+ return buildContextInternal(mtmv, queryUsedPartitions, null);
+ }
+
+ public static MTMVRefreshContext buildContext(MTMV mtmv, Map<List<String>,
Set<String>> queryUsedPartitions,
+ Map<MvccTableInfo, MvccSnapshot> pinnedSnapshots) throws
AnalysisException {
+ Map<MvccTableInfo, MvccSnapshot> snapshotCopy = new
LinkedHashMap<>(pinnedSnapshots);
+ return buildContextInternal(mtmv, queryUsedPartitions, snapshotCopy);
+ }
+
+ private static MTMVRefreshContext buildContextInternal(MTMV mtmv,
+ Map<List<String>, Set<String>> queryUsedPartitions,
+ Map<MvccTableInfo, MvccSnapshot> pinnedSnapshots) throws
AnalysisException {
+ MTMVRefreshContext context = new MTMVRefreshContext(mtmv,
pinnedSnapshots);
+ context.partitionMappings =
mtmv.calculatePartitionMappings(queryUsedPartitions, pinnedSnapshots);
context.baseVersions = MTMVPartitionUtil.getBaseVersions(mtmv,
context.partitionMappings);
return context;
}
+ private Optional<MvccSnapshot> resolveSnapshot(MTMVRelatedTableIf table) {
Review Comment:
[P1] Apply the task pin to non-PCT table snapshots too. This resolver now
keeps mapping and PCT partition snapshots at S1, but
`MTMVPartitionUtil.getTableSnapshotFromContext` still calls
`MvccUtil.getSnapshotFromContext` directly. During `MTMVTask.run` the outer
context has no `StatementContext`, so an Iceberg/Paimon non-PCT base can
materialize latest S2 here; each later refresh SQL explicitly scans the task
pin S1, while `generatePartitionSnapshots` persists the cached S2. The MV can
then be considered fresh at S2 and used for rewrite although S2 was never
materialized. Reuse this context resolver for table-level snapshots and add a
non-PCT S1-to-S2 task interleaving test.
##########
fe/fe-connector/fe-connector-hms/src/main/java/org/apache/doris/connector/hms/CachingHmsClient.java:
##########
@@ -203,76 +210,292 @@ public List<String> listPartitionNamesFresh(String
dbName, String tableName, int
@Override
public List<HmsPartitionInfo> getPartitions(String dbName, String
tableName, List<String> partNames) {
- if (partNames == null || partNames.isEmpty()) {
- return Collections.emptyList();
- }
- // Per-partition assembly (Trino CachingHiveMetastore / legacy
HiveExternalMetaCache shape): serve each
- // requested partition from its own entry and fetch only the misses in
ONE delegate round-trip, so
- // overlapping requests share partition objects and the capacity
bounds partition OBJECTS, not
- // request-lists. Correctness is independent of name-parse fidelity: a
LOOKUP is keyed by the requested
- // name parsed to values, but a STORE is always keyed by the
partition's OWN values, so a name whose
- // parse diverges (a rare escaped value) simply misses and is
re-fetched — never a wrong or dropped
- // partition. Callers consume the result as a SET (they never rely on
order or 1:1 name↔result
- // correspondence — the delegate get_partitions_by_names never
guaranteed either).
- List<HmsPartitionInfo> result = new ArrayList<>(partNames.size());
- List<String> missNames = null;
- for (String name : partNames) {
- HmsPartitionInfo hit =
- partitionsCache.getIfPresent(new PartitionKey(dbName,
tableName, toPartitionValues(name)));
+ return getPartitionsWithStats(dbName, tableName,
partNames).getPartitions();
+ }
+
+ @Override
+ public HmsPartitionBatchResult getPartitionsWithStats(
+ String dbName, String tableName, List<String> partNames) {
+ return getPartitionsWithStats(dbName, tableName, partNames, false);
+ }
+
+ @Override
+ public List<HmsPartitionInfo> getExistingPartitions(
+ String dbName, String tableName, List<String> partNames) {
+ return getExistingPartitionsWithStats(dbName, tableName,
partNames).getPartitions();
+ }
+
+ @Override
+ public HmsPartitionBatchResult getExistingPartitionsWithStats(
+ String dbName, String tableName, List<String> partNames) {
+ return getPartitionsWithStats(dbName, tableName, partNames, true);
+ }
+
+ private HmsPartitionBatchResult getPartitionsWithStats(
+ String dbName, String tableName, List<String> partNames, boolean
allowMissing) {
+ long logicalStartNanos = System.nanoTime();
+ if (partNames.isEmpty()) {
+ HmsPartitionBatchStats stats = HmsPartitionBatchStats.builder()
+ .logicalElapsedNanos(System.nanoTime() - logicalStartNanos)
+ .build();
+ return new HmsPartitionBatchResult(Collections.emptyList(), stats);
+ }
+ HmsPartitionRequest request = new HmsPartitionRequest(dbName,
tableName, partNames);
+ // Keep the existing cache policy: aggregate every miss into one
logical delegate request and publish
+ // only after that request succeeds. Reassemble from partition
identities afterwards because a mixed
+ // hit/miss request must preserve the caller's exact order even when
HMS returns a different order.
+ List<List<String>> requestedValues = new ArrayList<>(partNames.size());
+ Map<List<String>, HmsPartitionInfo> resultByIdentity = new HashMap<>();
+ List<HmsPartitionIdentity.ParsedPartitionName> misses = new
ArrayList<>();
+ for (HmsPartitionIdentity.ParsedPartitionName partition :
request.getPartitions()) {
+ List<String> values = partition.getValues();
+ requestedValues.add(values);
+ HmsPartitionInfo hit = partitionsCache.getIfPresent(new
PartitionKey(dbName, tableName, values));
if (hit != null) {
- result.add(hit);
+ resultByIdentity.put(values, hit);
} else {
- if (missNames == null) {
- missNames = new ArrayList<>();
+ misses.add(partition);
+ }
+ }
+ PartitionStatsAccumulator physicalStats = new
PartitionStatsAccumulator();
+ if (!misses.isEmpty()) {
+ try {
+ loadMissingPartitions(dbName, tableName, allowMissing, misses,
+ resultByIdentity, physicalStats);
+ } catch (HmsClientException e) {
+ HmsPartitionBatchStats failedStats =
e.getPartitionBatchStats();
+ if (failedStats != null) {
+ physicalStats.add(failedStats);
+ e.withPartitionBatchStats(physicalStats.build(
+ partNames.size(), System.nanoTime() -
logicalStartNanos));
}
- missNames.add(name);
+ throw e;
+ }
+ }
+ List<HmsPartitionInfo> result = new ArrayList<>(partNames.size());
+ for (int i = 0; i < requestedValues.size(); i++) {
+ List<String> values = requestedValues.get(i);
+ if (allowMissing && !resultByIdentity.containsKey(values)) {
+ continue;
+ }
+ HmsPartitionInfo partition = resultByIdentity.get(values);
+ if (partition == null) {
+ throw HmsPartitionResultException.builder(partNames.size(),
resultByIdentity.size())
+ .missing(request.getPartitions().get(i).getName())
+ .build();
}
+ result.add(partition);
}
- if (missNames != null) {
- // Capture the invalidation generation BEFORE the delegate RPC so
a REFRESH (flush) that races this
- // in-flight cold-cache fetch does not get silently undone by
re-caching the pre-refresh partitions.
- // The pre-D2 code went through partitionsCache.get(key, loader)
-> getWithManualLoad, which had this
- // guard; the per-partition put must restore it
(getTable/listPartitionNames/getTableColumnStatistics
- // still use the guarded get path). The delegate results still
populate the RESULT list directly,
- // preserving the misparse->never-drop safety (only the CACHE put
is generation-guarded).
- try (MetaCache.BulkLoad<PartitionKey, HmsPartitionInfo> load =
- partitionsCache.beginBulkLoad(ScopePath.table(dbName,
tableName))) {
- for (HmsPartitionInfo info : delegate.getPartitions(dbName,
tableName, missNames)) {
- load.publish(new PartitionKey(dbName, tableName,
info.getValues()), info);
- result.add(info);
+ HmsPartitionBatchStats stats = physicalStats.build(
+ partNames.size(), System.nanoTime() - logicalStartNanos);
+ return new HmsPartitionBatchResult(result, stats);
+ }
+
+ private void loadMissingPartitions(String dbName, String tableName,
boolean allowMissing,
+ List<HmsPartitionIdentity.ParsedPartitionName> initialMisses,
+ Map<List<String>, HmsPartitionInfo> resultByIdentity,
+ PartitionStatsAccumulator physicalStats) {
+ List<HmsPartitionIdentity.ParsedPartitionName> pending = initialMisses;
+ while (!pending.isEmpty()) {
+ // Elect one owner per missing identity. One caller can own a
batch and wait on identities owned by
+ // another caller, so partially overlapping requests still issue
one transport load per identity.
+ PartitionLoadBatch ownedBatch = new
PartitionLoadBatch(allowMissing);
+ List<PartitionLoadRegistration> owned = new ArrayList<>();
+ Map<PartitionLoadBatch, List<PartitionLoadRegistration>> waiting =
new IdentityHashMap<>();
+ try {
+ for (HmsPartitionIdentity.ParsedPartitionName partition :
pending) {
+ registerPartitionLoad(dbName, tableName, partition,
ownedBatch,
+ resultByIdentity, owned, waiting);
}
+ afterPartitionLoadRegistrationForTest();
+ if (!owned.isEmpty()) {
+ loadOwnedPartitions(dbName, tableName, allowMissing,
registrationsToPartitions(owned),
+ resultByIdentity, physicalStats, ownedBatch);
+ }
+ ownedBatch.complete(null);
+ } catch (RuntimeException | Error failure) {
+ ownedBatch.complete(failure);
+ releaseWaitingBatches(waiting.keySet());
+ throw failure;
+ } finally {
+ releaseOwnedPartitionLoads(ownedBatch);
}
+ List<HmsPartitionIdentity.ParsedPartitionName> retries = new
ArrayList<>();
+ consumeWaitingBatches(waiting, resultByIdentity, retries,
allowMissing);
+ pending = retries;
}
- return result;
}
- /**
- * Splits a Hive partition name ("c1=a/c2=b") into its ordered values
("a", "b"), unescaping each via
- * Hive's {@code FileUtils} (already a hms-module dependency — {@code
HmsEventParser} uses it). Semantics
- * match the write path's {@code HiveWriteUtils.toPartitionValues}, so
scan and write correlate partitions
- * identically. Only used to build the per-partition LOOKUP key: a parse
that diverges from the stored
- * partition's own values just misses and re-fetches (never a
wrong/dropped partition), so this is a
- * hit-rate optimization, not a correctness dependency.
- */
- private static List<String> toPartitionValues(String partitionName) {
- List<String> values = new ArrayList<>();
- int start = 0;
+ void afterPartitionLoadRegistrationForTest() {
+ }
+
+ void beforePartitionLoadElectionForTest() {
+ }
+
+ void afterPartitionLoadOwnershipForTest() {
+ }
+
+ int inFlightPartitionLoadCountForTest() {
+ return inFlightPartitionLoads.size();
+ }
+
+ private void registerPartitionLoad(String dbName, String tableName,
+ HmsPartitionIdentity.ParsedPartitionName partition,
PartitionLoadBatch ownedBatch,
+ Map<List<String>, HmsPartitionInfo> resultByIdentity,
+ List<PartitionLoadRegistration> owned,
+ Map<PartitionLoadBatch, List<PartitionLoadRegistration>> waiting) {
+ PartitionKey key = new PartitionKey(dbName, tableName,
partition.getValues());
+ HmsPartitionInfo hit = partitionsCache.getIfPresent(key);
+ if (hit != null) {
+ resultByIdentity.put(partition.getValues(), hit);
+ return;
+ }
+ beforePartitionLoadElectionForTest();
while (true) {
- while (start < partitionName.length() &&
partitionName.charAt(start) != '=') {
- start++;
+ PartitionLoadBatch existing =
inFlightPartitionLoads.putIfAbsent(key, ownedBatch);
+ if (existing == null) {
+ ownedBatch.claimedKeys.add(key);
+ afterPartitionLoadOwnershipForTest();
+ hit = partitionsCache.getIfPresent(key);
+ if (hit == null) {
+ owned.add(new PartitionLoadRegistration(partition, key));
+ } else {
+ resultByIdentity.put(partition.getValues(), hit);
+ ownedBatch.claimedKeys.remove(key);
+ inFlightPartitionLoads.remove(key, ownedBatch);
+ }
+ return;
}
- start++;
- int end = start;
- while (end < partitionName.length() && partitionName.charAt(end)
!= '/') {
- end++;
+ List<PartitionLoadRegistration> registrations =
waiting.get(existing);
+ if (registrations != null) {
+ registrations.add(new PartitionLoadRegistration(partition,
key));
+ return;
}
- if (start > partitionName.length()) {
- break;
+ if (existing.tryRegisterWaiter()) {
+ waiting.computeIfAbsent(existing, ignored -> new ArrayList<>())
+ .add(new PartitionLoadRegistration(partition, key));
+ return;
+ }
+ inFlightPartitionLoads.remove(key, existing);
+ }
+ }
+
+ private void loadOwnedPartitions(String dbName, String tableName, boolean
allowMissing,
+ List<HmsPartitionIdentity.ParsedPartitionName> owned,
+ Map<List<String>, HmsPartitionInfo> resultByIdentity,
+ PartitionStatsAccumulator physicalStats, PartitionLoadBatch
ownedBatch) {
+ List<String> names = new ArrayList<>(owned.size());
+ for (HmsPartitionIdentity.ParsedPartitionName partition : owned) {
+ names.add(partition.getName());
+ }
+ MetaCache.BulkLoad<PartitionKey, HmsPartitionInfo> load =
+ partitionsCache.beginBulkLoad(ScopePath.table(dbName,
tableName));
+ ownedBatch.setLoad(load);
+ HmsPartitionBatchResult loadedResult = allowMissing
+ ? delegate.getExistingPartitionsWithStats(dbName, tableName,
names)
+ : delegate.getPartitionsWithStats(dbName, tableName, names);
+ physicalStats.add(loadedResult.getStats());
+ List<HmsPartitionInfo> loaded = loadedResult.getPartitions();
+ for (HmsPartitionInfo info : loaded) {
+ PartitionKey key = new PartitionKey(dbName, tableName,
info.getValues());
+ load.publish(key, info);
+ resultByIdentity.put(info.getValues(), info);
+ ownedBatch.resolvedPartitions.put(key, info);
+ }
+ }
+
+ private static List<HmsPartitionIdentity.ParsedPartitionName>
registrationsToPartitions(
+ List<PartitionLoadRegistration> registrations) {
+ List<HmsPartitionIdentity.ParsedPartitionName> partitions = new
ArrayList<>(registrations.size());
+ for (PartitionLoadRegistration registration : registrations) {
+ partitions.add(registration.partition);
+ }
+ return partitions;
+ }
+
+ private void releaseOwnedPartitionLoads(PartitionLoadBatch ownedBatch) {
+ for (PartitionKey key : ownedBatch.claimedKeys) {
+ inFlightPartitionLoads.remove(key, ownedBatch);
+ }
+ ownedBatch.releaseOwner();
+ }
+
+ private static void releaseWaitingBatches(Set<PartitionLoadBatch> batches)
{
+ for (PartitionLoadBatch batch : batches) {
+ batch.releaseWaiter();
+ }
+ }
+
+ private void consumeWaitingBatches(
+ Map<PartitionLoadBatch, List<PartitionLoadRegistration>> waiting,
+ Map<List<String>, HmsPartitionInfo> resultByIdentity,
+ List<HmsPartitionIdentity.ParsedPartitionName> retries, boolean
allowMissing) {
+ List<Map.Entry<PartitionLoadBatch, List<PartitionLoadRegistration>>>
entries =
+ new ArrayList<>(waiting.entrySet());
+ for (int i = 0; i < entries.size(); i++) {
+ try {
+ Map.Entry<PartitionLoadBatch, List<PartitionLoadRegistration>>
entry = entries.get(i);
+ consumeWaitingBatch(entry.getKey(), entry.getValue(),
resultByIdentity, retries, allowMissing);
+ } catch (RuntimeException | Error failure) {
+ for (int remaining = i + 1; remaining < entries.size();
remaining++) {
+ entries.get(remaining).getKey().releaseWaiter();
+ }
+ throw failure;
}
-
values.add(FileUtils.unescapePathName(partitionName.substring(start, end)));
- start = end + 1;
}
- return values;
+ }
+
+ private void consumeWaitingBatch(PartitionLoadBatch batch,
+ List<PartitionLoadRegistration> registrations,
+ Map<List<String>, HmsPartitionInfo> resultByIdentity,
+ List<HmsPartitionIdentity.ParsedPartitionName> retries, boolean
allowMissing) {
+ try {
+ Throwable failure = batch.await();
+ if (failure != null) {
+ if (batch.allowMissing != allowMissing
Review Comment:
[P2] Share request-independent terminal failures with partial waiters. This
compatibility check runs before the failure is classified, so a waiter covering
a subset (or using the other missing-result contract) retries even
pool/client-creation, authentication, connection failures, and `Error`. For one
owner of N cold keys plus N singleton waiters, a global owner failure releases
every waiter into a separate delegate attempt under the same outage, defeating
single-flight and potentially saturating the metadata pool. Only
result-integrity failures can depend on identities outside a waiter's request;
propagate request-independent failures and `Error` to all overlapping waiters,
and add a broad-owner/singleton-waiters outage test.
--
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]