This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new be63e5f0cc7 [improvement](fe) Estimate wide metadata-cache values one
element at a time (#69558)
be63e5f0cc7 is described below
commit be63e5f0cc7a6976697d42c97810aa34c9a28ef9
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sun Oct 25 08:05:29 2026 +0000
[improvement](fe) Estimate wide metadata-cache values one element at a time
(#69558)
### What problem does this PR solve?
Issue Number: None
Related PR: #66717, #67726
Problem Summary:
**In short.** With a metadata-cache weight limit set, the cached
metadata of a wide table (thousands of columns), a long name list or a
long partition list was never cached: its size estimate gave up and the
value was reloaded on every statement. This PR weighs such values one
element at a time, so they are admitted and charged for their size,
while the safety bound of the estimate stays in place.
**Background.** Master (#67726) lets the metadata caches of external
catalogs be bounded by weight: the FE-global
`external_meta_cache_max_weight`, a catalog's `meta.cache.max-weight`,
or an entry's `meta.cache.<engine>.<entry>.max-weight`. Once any of them
applies, each value is weighed before it is admitted. Several entries
use the complete walk of `ReflectiveObjectSizeEstimator`, which visits
every object of the value and gives up after 10,000 of them, so that one
pathological value cannot cost unbounded CPU and temporary memory. An
estimate that gives up is incomplete, and an incomplete value is
returned to the query but not cached. branch-4.x (#66717) governs only a
few entries and weighs them with per-element formulas, so it has no such
limit.
**The problem, and what it cost.** The 10,000-visit budget is reached by
values that are large only because of one list, long before they are
large in bytes. Measured on master, the first value that never enters a
weighted cache:
| Entry | Value | Never cached from |
|---|---|---|
| `hms-table` | Hive table | 995 INT / 382 `struct<a:int,b:string>`
columns |
| `paimon-table`, `paimon-schema-at` | Paimon schema | 2,497 INT / 908
`ROW<a INT, b STRING>` columns |
| fe-core `schema` (`default`, `doris` engines) | schema of any other
external catalog | 3,332 INT / 832 struct columns |
| `adbc-table-schema` | Arrow schema | 2,499 INT / 1,110 struct fields |
| `adbc-table-names`, `adbc-namespaces` | name list | 9,999 names |
| `max-compute-partition` | partition list | 1,249 three-level
partitions |
For such a table every statement goes back to the source (an HMS call, a
Paimon schema file read, an ODPS partition listing, an ADBC metadata
call), and `weight_reject_count` in
`information_schema.catalog_meta_cache_statistics` keeps growing. In a
unit test with a 128 MiB global limit, three reads of a 2,000-column HMS
table loaded it three times and cached nothing.
**How this PR fixes it.**
- `ReflectiveObjectSizeEstimator.estimateCompletePerElement(root,
collections...)` walks `root` like the complete estimate, except that
each listed collection it reaches is counted as its own storage plus
every element estimated separately, each with its own 10,000-visit
budget. An element that appears in more than one of the listed
collections is counted once.
`MetaCacheSizeEstimators.reflectivePerElement(...)` and
`reflectiveEachElement()` wrap it as cache estimators.
- The affected entries use it: the HMS table (columns, partition keys),
the Paimon table schema and the schema-at snapshot (fields), the fe-core
schema cache (`SchemaCacheValue.getColumnLists()`: schema, and for
plugin tables also partition and display columns), the ADBC table schema
(fields) and name lists, and the MaxCompute partition list.
- The visit budget still bounds every element and the rest of the value,
so a value that reaches a huge shared structure still fails fast, and
the temporary identity set holds one element's graph at a time instead
of the whole value's. The whole-value walk and its budget are unchanged
for every other entry.
- Objects that elements share (for example a scalar type singleton) are
counted once per element, so the weight is somewhat above the retained
size (measured 1.1x to 1.6x) and never below it. A plugin table's
display columns reuse its column objects and are not counted twice.
**Results.**
| Value, under a weight limit | Before | After |
|---|---|---|
| 3,000-column HMS table (INT or struct columns) | estimate incomplete,
reloaded on every read | complete, loaded once, `weight_reject_count =
0` |
| 4,000-column Paimon table, INT or ROW columns; 4,000-field schema-at
snapshot | incomplete | complete, loaded once |
| fe-core schema with 4,000 INT + 1,000 struct columns | incomplete |
complete, loaded once |
| 3,000-field ADBC schema; 20,000 table names | incomplete | complete,
loaded once |
| 10,000 MaxCompute partitions | incomplete | complete, loaded once |
| single element larger than the visit budget | incomplete | incomplete
(unchanged) |
The new tests also check that the estimate is never below an unbounded
walk of the value and at most twice it, and that it doubles when the
column count doubles. With the per-element walk disabled, all nine new
wide-value tests fail; without the element de-duplication, a plugin
schema with reused display columns weighs twice as much (2,052,208 to
4,104,168 bytes) and its test fails.
---
.../doris/connector/adbc/AdbcMetadataCache.java | 10 +-
.../connector/adbc/AdbcMetadataCacheTest.java | 31 ++++++
.../connector/cache/MetaCacheSizeEstimators.java | 22 +++++
.../cache/ReflectiveObjectSizeEstimator.java | 49 ++++++++-
.../cache/ReflectiveObjectSizeEstimatorTest.java | 84 ++++++++++++++++
.../doris/connector/hms/HmsCacheSizeEstimator.java | 7 +-
.../connector/hms/HmsCacheSizeEstimatorTest.java | 110 +++++++++++++++++++++
.../maxcompute/MaxComputePartitionCache.java | 3 +-
.../maxcompute/MaxComputePartitionCacheTest.java | 32 ++++++
.../connector/paimon/PaimonCacheSizeEstimator.java | 8 +-
.../doris/connector/paimon/PaimonSchemaAtMemo.java | 5 +-
.../paimon/PaimonCacheSizeEstimatorTest.java | 23 +++++
.../connector/paimon/PaimonSchemaAtMemoTest.java | 34 +++++++
.../apache/doris/datasource/SchemaCacheValue.java | 9 ++
.../metacache/ExternalCatalogMetaCache.java | 12 ++-
.../plugin/PluginDrivenSchemaCacheValue.java | 6 ++
.../metacache/ExternalCatalogMetaCacheTest.java | 102 +++++++++++++++++++
17 files changed, 538 insertions(+), 9 deletions(-)
diff --git
a/fe/fe-connector/fe-connector-adbc/src/main/java/org/apache/doris/connector/adbc/AdbcMetadataCache.java
b/fe/fe-connector/fe-connector-adbc/src/main/java/org/apache/doris/connector/adbc/AdbcMetadataCache.java
index 27a7a3b2a4f..443137178d8 100644
---
a/fe/fe-connector/fe-connector-adbc/src/main/java/org/apache/doris/connector/adbc/AdbcMetadataCache.java
+++
b/fe/fe-connector/fe-connector-adbc/src/main/java/org/apache/doris/connector/adbc/AdbcMetadataCache.java
@@ -26,6 +26,7 @@ import org.apache.doris.connector.cache.ScopePath;
import org.apache.arrow.vector.types.pojo.Schema;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -104,22 +105,25 @@ public final class AdbcMetadataCache {
AdbcMetadataCache(CatalogMetaCache owner, Map<String, String> properties) {
this.owner = Objects.requireNonNull(owner, "owner can not be null");
CacheSpec spec = cacheSpec(properties);
+ // Long name lists and wide schemas would exhaust the visit budget of
a whole-value walk, so these values
+ // are estimated one namespace, name or field at a time.
this.namespaces = owner.create(MetaCacheDefinition
.<String, List<AdbcNamespace>>builder("adbc-namespaces", spec,
ignored -> ScopePath.catalog())
.budgetGroup(ENTRY)
- .sizeEstimator(MetaCacheSizeEstimators.reflective())
+ .sizeEstimator(MetaCacheSizeEstimators.reflectiveEachElement())
.build());
this.tableNames = owner.create(MetaCacheDefinition
.<TableNameKey, List<String>>builder("adbc-table-names", spec,
key -> ScopePath.database(key.dorisDbName))
.budgetGroup(ENTRY)
- .sizeEstimator(MetaCacheSizeEstimators.reflective())
+ .sizeEstimator(MetaCacheSizeEstimators.reflectiveEachElement())
.build());
this.tableSchemas = owner.create(MetaCacheDefinition
.<TableKey, Schema>builder("adbc-table-schema", spec,
key -> ScopePath.table(key.dorisDbName, key.table))
.budgetGroup(ENTRY)
- .sizeEstimator(MetaCacheSizeEstimators.reflective())
+ .sizeEstimator(MetaCacheSizeEstimators.reflectivePerElement(
+ schema ->
Collections.singletonList(schema.getFields())))
.build());
}
diff --git
a/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcMetadataCacheTest.java
b/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcMetadataCacheTest.java
index 5bc22477858..bb446ce030a 100644
---
a/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcMetadataCacheTest.java
+++
b/fe/fe-connector/fe-connector-adbc/src/test/java/org/apache/doris/connector/adbc/AdbcMetadataCacheTest.java
@@ -27,6 +27,7 @@ import org.apache.arrow.vector.types.pojo.Schema;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -72,6 +73,36 @@ class AdbcMetadataCacheTest {
}
}
+ @Test
+ void wideSchemasAndLongNameListsAreCachedUnderAWeightLimit() {
+ // A walk of the whole value ran out of the 10,000-visit budget at
about 2,500 Arrow fields or 10,000 names,
+ // so such a value was asked from the source on every statement once a
weight limit was set. MUTATION:
+ // estimating the value as one graph -> two calls and a rejected
admission -> red.
+ Map<String, String> properties = Map.of("meta.cache.max-weight",
"64MB");
+ try (CatalogMetaCache owner = CatalogMetaCache.managed(91090L, "adbc",
properties)) {
+ AdbcMetadataCache cache = new AdbcMetadataCache(owner, properties);
+ List<Field> fields = new ArrayList<>();
+ for (int i = 0; i < 3_000; i++) {
+ fields.add(Field.nullable("col_" + i, new ArrowType.Int(32,
true)));
+ }
+ List<String> tableNames = new ArrayList<>();
+ for (int i = 0; i < 20_000; i++) {
+ tableNames.add("table_" + i);
+ }
+ Counting<Schema> schema = new Counting<>(new Schema(fields));
+ Counting<List<String>> names = new Counting<>(tableNames);
+ for (int i = 0; i < 2; i++) {
+ cache.tableSchema(MAIN_T1, schema);
+ cache.tableNames(MAIN, names);
+ }
+
+ Assertions.assertEquals(1, schema.calls);
+ Assertions.assertEquals(1, names.calls);
+ Assertions.assertEquals(0L,
owner.entries().get("adbc-table-schema").metrics().getWeightRejectCount());
+ Assertions.assertEquals(0L,
owner.entries().get("adbc-table-names").metrics().getWeightRejectCount());
+ }
+ }
+
@Test
void rejectedSchemaIsReturnedWithoutBeingCached() {
Map<String, String> properties =
Map.of("meta.cache.adbc.metadata.max-weight", "1");
diff --git
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/MetaCacheSizeEstimators.java
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/MetaCacheSizeEstimators.java
index 8ec78638fa0..95757f9aaae 100644
---
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/MetaCacheSizeEstimators.java
+++
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/MetaCacheSizeEstimators.java
@@ -17,6 +17,10 @@
package org.apache.doris.connector.cache;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Function;
+
/** Shared estimators for small or structurally unknown metadata-cache
entries. */
public final class MetaCacheSizeEstimators {
private MetaCacheSizeEstimators() {
@@ -32,4 +36,22 @@ public final class MetaCacheSizeEstimators {
ReflectiveObjectSizeEstimator.estimateComplete(key),
ReflectiveObjectSizeEstimator.estimateComplete(value)));
}
+
+ /**
+ * Returns a {@link #reflective()} estimator that walks the long lists
{@code perElementCollections} picks from
+ * a value element by element (see {@link
ReflectiveObjectSizeEstimator#estimateCompletePerElement}), for values
+ * that grow with one list, such as a wide table's schema.
+ */
+ public static <K, V> MetaCacheSizeEstimator<K, V> reflectivePerElement(
+ Function<? super V, ? extends Collection<? extends Collection<?>>>
perElementCollections) {
+ return (key, value) ->
MetaCacheSizeEstimate.complete(JvmSizeUtils.saturatedAdd(
+ ReflectiveObjectSizeEstimator.estimateComplete(key),
+ ReflectiveObjectSizeEstimator.estimateCompletePerElement(value,
+ perElementCollections.apply(value).toArray(new
Collection<?>[0]))));
+ }
+
+ /** {@link #reflectivePerElement} for a value that is itself the long
list, such as a partition list. */
+ public static <K, V extends Collection<?>> MetaCacheSizeEstimator<K, V>
reflectiveEachElement() {
+ return reflectivePerElement(value -> Collections.singletonList(value));
+ }
}
diff --git
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimator.java
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimator.java
index 5f1d0c56e38..1d85a5d8c66 100644
---
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimator.java
+++
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimator.java
@@ -23,6 +23,7 @@ import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -48,7 +49,9 @@ import java.util.concurrent.ConcurrentHashMap;
* The diagnostic API samples collections and object arrays. The admission API
instead traverses every element
* up to a fixed visit budget: this catches skewed tail values while bounding
CPU and temporary identity-set
* memory. Strongly encapsulated reference fields, depth truncation, and
visit-budget exhaustion fail the
- * estimate so admission can reject the value without turning an incomplete
graph into a false low weight.
+ * estimate so admission can reject the value without turning an incomplete
graph into a false low weight. A value
+ * that is large only because of a few long lists, such as a wide table's
columns, is estimated with those lists
+ * walked element by element instead, so the budget bounds one element rather
than the whole value.
*
* <p>This is intentionally a construction-time safety net, not a Caffeine
hit-path weigher. Cache values should
* combine it with a type-specific estimate once, store the larger result, and
expose that stored number to
@@ -141,14 +144,36 @@ public final class ReflectiveObjectSizeEstimator {
return new Walker(Integer.MAX_VALUE, visitBudget).estimate(root,
maxDepth);
}
+ /**
+ * Completely estimates {@code root} like {@link
#estimateComplete(Object)}, except that each of
+ * {@code perElementCollections} that the walk reaches is estimated
element by element, every element with its
+ * own visit budget. A value that is large only because of such lists,
such as a wide table's columns or a long
+ * partition list, stays admissible, while the budget still bounds each
element and the rest of the graph, and
+ * the temporary identity set holds one element's graph at a time. Objects
that elements share are counted
+ * once per element, which can only over-estimate; an element in several
of the collections is counted once.
+ */
+ public static long estimateCompletePerElement(Object root,
Collection<?>... perElementCollections) {
+ Set<Object> collections = Collections.newSetFromMap(new
IdentityHashMap<>());
+ Collections.addAll(collections, perElementCollections);
+ return new Walker(Integer.MAX_VALUE, DEFAULT_COMPLETE_VISIT_BUDGET,
collections)
+ .estimate(root, DEFAULT_MAX_DEPTH);
+ }
+
private static final class Walker {
private final int sampleSize;
private final int visitBudget;
private final Map<Object, Boolean> visited = new IdentityHashMap<>();
+ private final Set<Object> perElementCollections;
+ private final Set<Object> estimatedElements =
Collections.newSetFromMap(new IdentityHashMap<>());
private Walker(int sampleSize, int visitBudget) {
+ this(sampleSize, visitBudget, Collections.emptySet());
+ }
+
+ private Walker(int sampleSize, int visitBudget, Set<Object>
perElementCollections) {
this.sampleSize = sampleSize;
this.visitBudget = visitBudget;
+ this.perElementCollections = perElementCollections;
}
private long estimate(Object value, int depth) {
@@ -159,6 +184,9 @@ public final class ReflectiveObjectSizeEstimator {
throw new IllegalStateException(
"Complete estimate exceeded visit budget " +
visitBudget);
}
+ if (perElementCollections.contains(value)) {
+ return estimateEachElement((Collection<?>) value, depth);
+ }
Class<?> type = value.getClass();
if (type.isEnum() || value instanceof Class<?>) {
@@ -230,6 +258,25 @@ public final class ReflectiveObjectSizeEstimator {
return add(bytes, scale(sampledBytes, samples, length));
}
+ private long estimateEachElement(Collection<?> values, int depth) {
+ int size = values.size();
+ long bytes = add(
+ JvmSizeUtils.instanceSize(values.getClass()),
+ collectionStorageBytes(values, size));
+ if (size == 0) {
+ return bytes;
+ }
+ if (depth == 0) {
+ throw incomplete(values.getClass());
+ }
+ for (Object element : values) {
+ if (estimatedElements.add(element)) {
+ bytes = add(bytes, new Walker(Integer.MAX_VALUE,
visitBudget).estimate(element, depth - 1));
+ }
+ }
+ return bytes;
+ }
+
private long estimateByteBuffer(ByteBuffer value, int depth) {
long bytes = JvmSizeUtils.instanceSize(value.getClass());
byte[] backing = JvmSizeUtils.byteBufferBackingArray(value);
diff --git
a/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimatorTest.java
b/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimatorTest.java
index edb3d3f843e..ccef17bec58 100644
---
a/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimatorTest.java
+++
b/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ReflectiveObjectSizeEstimatorTest.java
@@ -23,7 +23,9 @@ import org.junit.jupiter.api.Test;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.AbstractList;
+import java.util.ArrayList;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.concurrent.atomic.AtomicInteger;
@@ -129,6 +131,88 @@ class ReflectiveObjectSizeEstimatorTest {
Assertions.assertTrue(estimate.getIncompleteReason().startsWith("depth_limit:"));
}
+ @Test
+ void perElementEstimateCompletesALongListThatTheWholeValueWalkRejects() {
+ // A value that is large only because of one long list, such as a wide
table's columns, has the list walked
+ // element by element, each element under its own visit budget.
MUTATION: walking the list as part of the
+ // value's graph -> the visit budget runs out -> red.
+ Holder small = holderOf(1_000);
+ Holder large = holderOf(30_000);
+ MetaCacheSizeEstimate whole =
MetaCacheSizeEstimator.estimateSafely("whole_value",
+ () ->
MetaCacheSizeEstimate.complete(ReflectiveObjectSizeEstimator.estimateComplete(large)));
+ Assertions.assertFalse(whole.isComplete());
+
+ long smallBytes =
ReflectiveObjectSizeEstimator.estimateCompletePerElement(small, small.values);
+ long largeBytes =
ReflectiveObjectSizeEstimator.estimateCompletePerElement(large, large.values);
+
+ // The elements share no object, so the estimate is exactly an
unbounded walk of the value.
+
Assertions.assertEquals(ReflectiveObjectSizeEstimator.estimateComplete(large,
Integer.MAX_VALUE, 20),
+ largeBytes);
+ Assertions.assertTrue(largeBytes > smallBytes * 25, "the estimate must
grow with the list");
+ }
+
+ @Test
+ void perElementEstimateCountsAnElementInTwoOfTheListsOnce() {
+ // A schema's display columns reuse most of its column objects.
MUTATION: estimating every element of every
+ // listed collection -> the shared elements count twice -> red.
+ Holder holder = holderOf(20_000);
+ holder.view = new ArrayList<>(holder.values);
+
+
Assertions.assertEquals(ReflectiveObjectSizeEstimator.estimateComplete(holder,
Integer.MAX_VALUE, 20),
+
ReflectiveObjectSizeEstimator.estimateCompletePerElement(holder, holder.values,
holder.view));
+ }
+
+ @Test
+ void perElementEstimateStillBoundsEachElement() {
+ AtomicInteger reads = new AtomicInteger();
+ Holder holder = holderOf(10);
+ holder.values.add(new CountingList(reads));
+
+ MetaCacheSizeEstimate estimate =
MetaCacheSizeEstimator.estimateSafely("bounded_element",
+ () -> MetaCacheSizeEstimate.complete(
+
ReflectiveObjectSizeEstimator.estimateCompletePerElement(holder,
holder.values)));
+
+ Assertions.assertFalse(estimate.isComplete());
+ Assertions.assertTrue(reads.get() <= 10_001, "one element's walk must
stop at the visit budget");
+ }
+
+ @Test
+ void eachElementEstimatorWeighsAValueThatIsTheLongList() {
+ List<Object> partitions = holderOf(30_000).values;
+
+ MetaCacheSizeEstimate estimate = MetaCacheSizeEstimators
+ .<String, List<Object>>reflectiveEachElement().estimate("key",
partitions);
+
+ Assertions.assertTrue(estimate.isComplete(),
estimate.getIncompleteReason());
+
Assertions.assertEquals(ReflectiveObjectSizeEstimator.estimateComplete("key")
+ + ReflectiveObjectSizeEstimator.estimateComplete(partitions,
Integer.MAX_VALUE, 20),
+ estimate.getBytes());
+ }
+
+ private static Holder holderOf(int elements) {
+ Holder holder = new Holder();
+ for (int i = 0; i < elements; i++) {
+ holder.values.add(new Element("element-" + i, 1_000L + i));
+ }
+ return holder;
+ }
+
+ private static final class Holder {
+ private final String name = "holder";
+ private final List<Object> values = new ArrayList<>();
+ private List<Object> view;
+ }
+
+ private static final class Element {
+ private final String name;
+ private final Long id;
+
+ private Element(String name, Long id) {
+ this.name = name;
+ this.id = id;
+ }
+ }
+
private static final class CountingList extends AbstractList<String>
implements RandomAccess {
private final AtomicInteger reads;
diff --git
a/fe/fe-connector/fe-connector-hms/src/main/java/org/apache/doris/connector/hms/HmsCacheSizeEstimator.java
b/fe/fe-connector/fe-connector-hms/src/main/java/org/apache/doris/connector/hms/HmsCacheSizeEstimator.java
index 59dfd6596e2..10ea2908a32 100644
---
a/fe/fe-connector/fe-connector-hms/src/main/java/org/apache/doris/connector/hms/HmsCacheSizeEstimator.java
+++
b/fe/fe-connector/fe-connector-hms/src/main/java/org/apache/doris/connector/hms/HmsCacheSizeEstimator.java
@@ -32,7 +32,12 @@ final class HmsCacheSizeEstimator {
}
static MetaCacheSizeEstimate estimateTable(Object key, HmsTableInfo value)
{
- return complete(key, value);
+ // A whole-value walk of a wide table would exhaust the visit budget
and keep the table out of a weighted
+ // cache, so each column is estimated on its own.
+ return MetaCacheSizeEstimate.complete(add(
+ ReflectiveObjectSizeEstimator.estimateComplete(key),
+ ReflectiveObjectSizeEstimator.estimateCompletePerElement(
+ value, value.getColumns(), value.getPartitionKeys())));
}
static MetaCacheSizeEstimate estimatePartition(Object key,
HmsPartitionInfo value) {
diff --git
a/fe/fe-connector/fe-connector-hms/src/test/java/org/apache/doris/connector/hms/HmsCacheSizeEstimatorTest.java
b/fe/fe-connector/fe-connector-hms/src/test/java/org/apache/doris/connector/hms/HmsCacheSizeEstimatorTest.java
new file mode 100644
index 00000000000..824f442352a
--- /dev/null
+++
b/fe/fe-connector/fe-connector-hms/src/test/java/org/apache/doris/connector/hms/HmsCacheSizeEstimatorTest.java
@@ -0,0 +1,110 @@
+// 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.doris.connector.hms;
+
+import org.apache.doris.connector.cache.CacheSpec;
+import org.apache.doris.connector.cache.CatalogMetaCache;
+import org.apache.doris.connector.cache.MetaCache;
+import org.apache.doris.connector.cache.MetaCacheBudgetManager;
+import org.apache.doris.connector.cache.MetaCacheDefinition;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimate;
+import org.apache.doris.connector.cache.ReflectiveObjectSizeEstimator;
+import org.apache.doris.connector.cache.ScopePath;
+import org.apache.doris.connector.spi.ConnectorColumn;
+import org.apache.doris.connector.spi.ConnectorType;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalLong;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class HmsCacheSizeEstimatorTest {
+
+ @Test
+ void wideTablesAreWeighedCompletelyAndLinearly() {
+ // A walk of the whole table ran out of the 10,000-visit budget at 995
INT or 382 struct columns, so a wider
+ // table never entered a weighted cache. MUTATION: estimating the
table as one graph -> incomplete -> red.
+ for (String hiveType : List.of("int", "struct<a:int,b:string>")) {
+ MetaCacheSizeEstimate half =
HmsCacheSizeEstimator.estimateTable("db.wide", table(1_500, hiveType));
+ HmsTableInfo table = table(3_000, hiveType);
+ MetaCacheSizeEstimate full =
HmsCacheSizeEstimator.estimateTable("db.wide", table);
+
+ Assertions.assertTrue(full.isComplete(), hiveType + ": " +
full.getIncompleteReason());
+ // Never below an unbounded walk of the value; above it only by
the objects that columns share.
+ long walked =
ReflectiveObjectSizeEstimator.estimateComplete("db.wide", Integer.MAX_VALUE, 20)
+ + ReflectiveObjectSizeEstimator.estimateComplete(table,
Integer.MAX_VALUE, 20);
+ Assertions.assertTrue(full.getBytes() >= walked, hiveType + ": " +
full.getBytes() + " < " + walked);
+ Assertions.assertTrue(full.getBytes() <= walked * 2,
+ hiveType + ": " + full.getBytes() + " > 2 * " + walked);
+ double growth = (double) full.getBytes() / half.getBytes();
+ Assertions.assertTrue(growth > 1.8 && growth < 2.2, hiveType + ":
doubling the columns grew it " + growth);
+ }
+ }
+
+ @Test
+ void wideTableIsCachedUnderAWeightLimit() {
+ MetaCacheBudgetManager manager = new
MetaCacheBudgetManager(OptionalLong.of(128L << 20));
+ try (CatalogMetaCache owner = new CatalogMetaCache(manager, 1L,
"hive", Collections.emptyMap())) {
+ MetaCache<String, HmsTableInfo> entry =
owner.create(MetaCacheDefinition
+ .<String, HmsTableInfo>builder("hms-table",
CacheSpec.of(true, -1L, 100L),
+ ignored -> ScopePath.catalog())
+ .sizeEstimator(HmsCacheSizeEstimator::estimateTable)
+ .build());
+ HmsTableInfo wide = table(3_000, "struct<a:int,b:string>");
+ AtomicInteger loads = new AtomicInteger();
+ for (int i = 0; i < 3; i++) {
+ Assertions.assertSame(wide, entry.get("db.wide", ignored -> {
+ loads.incrementAndGet();
+ return wide;
+ }));
+ }
+
+ Assertions.assertEquals(1, loads.get());
+ Assertions.assertSame(wide, entry.getIfPresent("db.wide"));
+ Assertions.assertEquals(0L,
entry.metrics().getWeightRejectCount());
+ Assertions.assertTrue(entry.metrics().getEstimatedWeight() > 0L);
+ }
+ }
+
+ private static HmsTableInfo table(int columns, String hiveType) {
+ List<ConnectorColumn> dataColumns = new ArrayList<>(columns);
+ for (int i = 0; i < columns; i++) {
+ dataColumns.add(new ConnectorColumn("col_" + i,
HmsTypeMapping.toConnectorType(hiveType), "comment " + i,
+ true, null, true));
+ }
+ Map<String, String> parameters = new HashMap<>();
+ for (int i = 0; i < 10; i++) {
+ parameters.put("param_" + i, "value_" + i);
+ }
+ return
HmsTableInfo.builder().dbName("db").tableName("wide").tableType("EXTERNAL_TABLE")
+ .parameters(parameters).columns(dataColumns)
+ .partitionKeys(Collections.singletonList(
+ new ConnectorColumn("dt", ConnectorType.of("STRING"),
null, true, null, true)))
+ .location("hdfs://nn/warehouse/db/wide")
+
.inputFormat("org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat")
+
.outputFormat("org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat")
+
.serializationLib("org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe")
+ .build();
+ }
+}
diff --git
a/fe/fe-connector/fe-connector-maxcompute/src/main/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCache.java
b/fe/fe-connector/fe-connector-maxcompute/src/main/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCache.java
index 0f1aaeddccd..34a22b5dfd8 100644
---
a/fe/fe-connector/fe-connector-maxcompute/src/main/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCache.java
+++
b/fe/fe-connector/fe-connector-maxcompute/src/main/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCache.java
@@ -104,7 +104,8 @@ public class MaxComputePartitionCache {
.<PartitionKey,
List<PartitionSpec>>builder("max-compute-partition", spec,
key -> ScopePath.table(key.dbName, key.tableName))
.budgetGroup(ENTRY_PARTITION)
- .sizeEstimator(MetaCacheSizeEstimators.reflective())
+ // A long partition list would exhaust the visit budget of a
whole-value walk.
+ .sizeEstimator(MetaCacheSizeEstimators.reflectiveEachElement())
.build());
this.lister = Objects.requireNonNull(lister, "lister can not be null");
}
diff --git
a/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
b/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
index 61b8d2acaf7..ad85d2a09fa 100644
---
a/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
+++
b/fe/fe-connector/fe-connector-maxcompute/src/test/java/org/apache/doris/connector/maxcompute/MaxComputePartitionCacheTest.java
@@ -142,6 +142,38 @@ public class MaxComputePartitionCacheTest {
}
}
+ @Test
+ public void longPartitionListIsCachedUnderAWeightLimit() throws Exception {
+ // A walk of the whole list ran out of the 10,000-visit budget at
about 1,250 three-level partitions, so a
+ // longer listing was fetched from ODPS on every statement once a
weight limit was set. MUTATION: estimating
+ // the list as one graph -> two loads and a rejected admission -> red.
+ Odps odps = new Odps(new AliyunAccount("ak", "sk"));
+ Constructor<Partition> constructor =
Partition.class.getDeclaredConstructor(
+ PartitionSpec.class, String.class, String.class, String.class,
Odps.class);
+ constructor.setAccessible(true);
+ List<Partition> partitions = new ArrayList<>();
+ for (int i = 0; i < 10_000; i++) {
+ PartitionSpec spec = new PartitionSpec(
+ "ds='2026-09-" + (i % 30) + "',hr='" + (i % 24) + "',id='"
+ i + "'");
+ partitions.add(constructor.newInstance(spec, "project", null,
"table", odps));
+ }
+ Map<String, String> props = Map.of("meta.cache.max-weight", "64MB");
+ AtomicInteger loads = new AtomicInteger();
+ try (CatalogMetaCache owner = CatalogMetaCache.managed(91091L,
"max_compute", props)) {
+ MaxComputePartitionCache cache = new
MaxComputePartitionCache(owner, props, (db, table) -> {
+ loads.incrementAndGet();
+ return partitions;
+ });
+ for (int i = 0; i < 2; i++) {
+ Assertions.assertEquals(10_000, cache.getPartitions("db",
"t").size());
+ }
+
+ Assertions.assertEquals(1, loads.get());
+ Assertions.assertEquals(0L,
+
owner.entries().get("max-compute-partition").metrics().getWeightRejectCount());
+ }
+ }
+
// ==================== caching: hit / miss keyed by (db, table)
====================
@Test
diff --git
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimator.java
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimator.java
index d5260ed8549..64ee96eb7ea 100644
---
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimator.java
+++
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimator.java
@@ -135,7 +135,7 @@ final class PaimonCacheSizeEstimator {
return add(bytes, estimateFileStoreTable(
((DelegatedFileStoreTable) table).wrapped(), visited));
}
- bytes = add(bytes, estimateCompleteOnce(table.schema(), visited));
+ bytes = add(bytes, estimateSchemaOnce(table.schema(), visited));
bytes = add(bytes, estimateStoreGrowth(table.schema()));
bytes = add(bytes, estimateTableFileIO(table.fileIO(), visited));
bytes = add(bytes, estimatePath(table.location(), visited));
@@ -200,6 +200,12 @@ final class PaimonCacheSizeEstimator {
return add(JvmSizeUtils.instanceSize(fileIO.getClass()),
FILE_IO_BYTES);
}
+ /** A whole-value walk of a wide schema would exhaust the visit budget, so
each field is estimated on its own. */
+ private static long estimateSchemaOnce(TableSchema schema, Set<Object>
visited) {
+ return !visited.add(schema)
+ ? 0L :
ReflectiveObjectSizeEstimator.estimateCompletePerElement(schema,
schema.fields());
+ }
+
private static long estimateCompleteOnce(Object value, Set<Object>
visited) {
return value == null || !visited.add(value)
? 0L : ReflectiveObjectSizeEstimator.estimateComplete(value);
diff --git
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemo.java
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemo.java
index d1cc19beff9..3f9336c631e 100644
---
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemo.java
+++
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemo.java
@@ -24,6 +24,7 @@ import org.apache.doris.connector.cache.MetaCacheDefinition;
import org.apache.doris.connector.cache.MetaCacheSizeEstimators;
import org.apache.doris.connector.cache.ScopePath;
+import java.util.Collections;
import java.util.Objects;
import java.util.function.Supplier;
@@ -70,7 +71,9 @@ final class PaimonSchemaAtMemo {
.<MemoKey, PaimonCatalogOps.PaimonSchemaSnapshot>builder(
"paimon-schema-at", spec,
key -> ScopePath.table(key.databaseName,
key.tableName))
- .sizeEstimator(MetaCacheSizeEstimators.reflective())
+ // A wide schema's fields would exhaust the visit budget of a
whole-value walk.
+ .sizeEstimator(MetaCacheSizeEstimators.reflectivePerElement(
+ snapshot ->
Collections.singletonList(snapshot.fields())))
.build());
}
diff --git
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimatorTest.java
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimatorTest.java
index a7399c41a76..84e1ff99709 100644
---
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimatorTest.java
+++
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimatorTest.java
@@ -109,6 +109,16 @@ class PaimonCacheSizeEstimatorTest {
table("nested_many", 0, 0, 0, 200, null));
}
+ @Test
+ void wideSchemasAreWeighedCompletely() throws Exception {
+ // A walk of the whole schema ran out of the 10,000-visit budget at
about 2,500 INT or 900 ROW columns, so a
+ // wider table never entered a weighted cache. MUTATION: estimating
the schema as one graph -> incomplete
+ // -> red.
+ assertGrownDelta("wide fields", table("wide_fields_2000", 2_000, 0, 0,
0, null),
+ table("wide_fields_4000", 4_000, 0, 0, 0, null));
+ weight(rowColumnsTable("wide_rows_4000", 4_000));
+ }
+
@Test
void everyTableCarriesAnAllowanceForItsFileIo() throws Exception {
// A REST catalog gives each table its own RESTTokenFileIO or
ResolvingFileIO and a Hive catalog a FileIO
@@ -289,6 +299,19 @@ class PaimonCacheSizeEstimatorTest {
return FileStoreTableFactory.create(fileIO, path);
}
+ private FileStoreTable rowColumnsTable(String name, int columns) throws
Exception {
+ Schema.Builder builder = Schema.newBuilder().column("part",
DataTypes.INT());
+ for (int i = 0; i < columns; i++) {
+ builder.column("row_" + i, DataTypes.ROW(DataTypes.FIELD(0, "a",
DataTypes.INT()),
+ DataTypes.FIELD(1, "b", DataTypes.STRING())));
+ }
+ builder.partitionKeys("part");
+ LocalFileIO fileIO = LocalFileIO.create();
+ Path path = new Path(warehouse.resolve(name).toUri());
+ new FileSystemSchemaManager(fileIO, path).createTable(builder.build());
+ return FileStoreTableFactory.create(fileIO, path);
+ }
+
private static RowType rowOf(int fieldCount) {
List<DataField> fields = new ArrayList<>();
for (int i = 0; i < fieldCount; i++) {
diff --git
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemoTest.java
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemoTest.java
index 5b3aecd623c..cfde3692623 100644
---
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemoTest.java
+++
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonSchemaAtMemoTest.java
@@ -17,10 +17,17 @@
package org.apache.doris.connector.paimon;
+import org.apache.doris.connector.cache.CatalogMetaCache;
+
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -40,6 +47,33 @@ public class PaimonSchemaAtMemoTest {
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList());
}
+ @Test
+ public void wideSchemaIsCachedUnderAWeightLimit() {
+ // A walk of the whole snapshot ran out of the 10,000-visit budget at
about 2,500 fields, so a wider schema
+ // was read again on every time-travel query once a weight limit was
set. MUTATION: estimating the snapshot
+ // as one graph -> two loads and a rejected admission -> red.
+ Map<String, String> properties = Map.of("meta.cache.max-weight",
"64MB");
+ try (CatalogMetaCache owner = CatalogMetaCache.managed(91201L,
"paimon", properties)) {
+ PaimonSchemaAtMemo memo = new PaimonSchemaAtMemo(owner, 100);
+ List<DataField> fields = new ArrayList<>();
+ for (int i = 0; i < 4_000; i++) {
+ fields.add(new DataField(i, "col_" + i, DataTypes.INT(),
"comment " + i));
+ }
+ PaimonCatalogOps.PaimonSchemaSnapshot wide = new
PaimonCatalogOps.PaimonSchemaSnapshot(
+ fields, Collections.emptyList(), Collections.emptyList());
+ AtomicInteger loads = new AtomicInteger();
+ for (int i = 0; i < 2; i++) {
+ memo.getOrLoad(handle("db", "t"), 5L, () -> {
+ loads.incrementAndGet();
+ return wide;
+ });
+ }
+
+ Assertions.assertEquals(1, loads.get());
+ Assertions.assertEquals(0L,
owner.entries().get("paimon-schema-at").metrics().getWeightRejectCount());
+ }
+ }
+
@Test
public void sameKeyLoadsOnce() {
PaimonSchemaAtMemo memo = new PaimonSchemaAtMemo(100);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/SchemaCacheValue.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/SchemaCacheValue.java
index ac7c55105af..5b641ff6df5 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/SchemaCacheValue.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/SchemaCacheValue.java
@@ -19,6 +19,7 @@ package org.apache.doris.datasource;
import org.apache.doris.catalog.Column;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -40,6 +41,14 @@ public class SchemaCacheValue {
return schema;
}
+ /**
+ * The column lists that grow with the table's width. The cache's size
estimator walks them column by column, so
+ * a wide table stays admissible to a weighted schema cache.
+ */
+ public List<List<Column>> getColumnLists() {
+ return Collections.singletonList(schema);
+ }
+
public void validateSchema() throws IllegalArgumentException {
Set<String> columnNames = new HashSet<>();
for (Column column : schema) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCache.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCache.java
index b7f9809e264..4a639d8b202 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCache.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCache.java
@@ -22,6 +22,7 @@ import org.apache.doris.common.Config;
import org.apache.doris.connector.cache.CacheSpec;
import org.apache.doris.connector.cache.MetaCache;
import org.apache.doris.connector.cache.MetaCacheDefinition;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimator;
import org.apache.doris.connector.cache.MetaCacheSizeEstimators;
import org.apache.doris.datasource.CacheException;
import org.apache.doris.datasource.CatalogIf;
@@ -30,6 +31,7 @@ import org.apache.doris.datasource.ExternalTable;
import org.apache.doris.datasource.NameMapping;
import org.apache.doris.datasource.SchemaCacheValue;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import java.time.Duration;
@@ -290,7 +292,7 @@ public abstract class ExternalCatalogMetaCache implements
ExternalMetaCache {
Function<K, V> loader = wrapSchemaValidator(entryDef.getLoader(),
entryDef.getValueType());
MetaCacheDefinition.Builder<K, V> builder = MetaCacheDefinition.<K,
V>builder(
entryDef.getName(), cacheSpec,
entryDef.getInvalidation()::scope)
- .sizeEstimator(MetaCacheSizeEstimators.reflective());
+ .sizeEstimator(sizeEstimator(entryDef.getValueType()));
if (loader != null) {
builder.loader(loader);
}
@@ -301,6 +303,14 @@ public abstract class ExternalCatalogMetaCache implements
ExternalMetaCache {
return group.owner().create(builder.build());
}
+ /** A whole-value walk of a wide table's schema would exhaust the visit
budget, so it goes column by column. */
+ @VisibleForTesting
+ static <K, V> MetaCacheSizeEstimator<K, V> sizeEstimator(Class<V>
valueType) {
+ return SchemaCacheValue.class.isAssignableFrom(valueType)
+ ? MetaCacheSizeEstimators.reflectivePerElement(value ->
((SchemaCacheValue) value).getColumnLists())
+ : MetaCacheSizeEstimators.reflective();
+ }
+
private <K, V> Function<K, V> wrapSchemaValidator(Function<K, V> loader,
Class<V> valueType) {
if (loader == null) {
return null;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenSchemaCacheValue.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenSchemaCacheValue.java
index 4e7650aa2a8..71e1b646b63 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenSchemaCacheValue.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenSchemaCacheValue.java
@@ -21,6 +21,7 @@ import org.apache.doris.catalog.Column;
import org.apache.doris.connector.spi.ConnectorCapability;
import org.apache.doris.datasource.SchemaCacheValue;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -141,4 +142,9 @@ public class PluginDrivenSchemaCacheValue extends
SchemaCacheValue {
public List<Column> getDisplayColumns() {
return displayColumns;
}
+
+ @Override
+ public List<List<Column>> getColumnLists() {
+ return Arrays.asList(schema, partitionColumns, displayColumns);
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCacheTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCacheTest.java
index 34b989380f3..7524288f2cb 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCacheTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/metacache/ExternalCatalogMetaCacheTest.java
@@ -19,20 +19,32 @@ package org.apache.doris.datasource.metacache;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.StructField;
+import org.apache.doris.catalog.StructType;
import org.apache.doris.connector.cache.CacheSpec;
import org.apache.doris.connector.cache.MetaCache;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimate;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimator;
+import org.apache.doris.connector.cache.ReflectiveObjectSizeEstimator;
import org.apache.doris.datasource.NameMapping;
import org.apache.doris.datasource.SchemaCacheKey;
import org.apache.doris.datasource.SchemaCacheValue;
+import org.apache.doris.datasource.plugin.PluginDrivenSchemaCacheValue;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
public class ExternalCatalogMetaCacheTest {
@@ -145,6 +157,96 @@ public class ExternalCatalogMetaCacheTest {
}
}
+ @Test
+ public void testWideSchemaIsCachedUnderAWeightLimit() {
+ // A walk of the whole value ran out of the 10,000-visit budget at
about 3,300 INT or 830 struct columns, so a
+ // wider table's schema was loaded again on every statement once a
weight limit was set. MUTATION: estimating
+ // the schema as one graph -> two loads and a rejected admission ->
red.
+ ExecutorService refreshExecutor = Executors.newSingleThreadExecutor();
+ try {
+ SchemaCacheValue wide = new SchemaCacheValue(wideColumns(4_000,
1_000));
+ AtomicInteger loads = new AtomicInteger();
+ WideSchemaMetaCache cache = new
WideSchemaMetaCache(refreshExecutor, () -> {
+ loads.incrementAndGet();
+ return wide;
+ });
+ Map<String, String> properties = Maps.newHashMap();
+ properties.put("meta.cache.max-weight", "64MB");
+ cache.initCatalog(2L, properties);
+ MetaCache<SchemaCacheKey, SchemaCacheValue> schemaEntry =
cache.entry(
+ 2L, "schema", SchemaCacheKey.class,
SchemaCacheValue.class);
+ SchemaCacheKey key = new
SchemaCacheKey(NameMapping.createForTest(2L, "db1", "wide"));
+ for (int i = 0; i < 2; i++) {
+ Assertions.assertSame(wide, schemaEntry.get(key));
+ }
+
+ Assertions.assertEquals(1, loads.get());
+ Assertions.assertEquals(0L,
schemaEntry.metrics().getWeightRejectCount());
+ cache.invalidateCatalog(2L);
+ } finally {
+ refreshExecutor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void
testSchemaEstimateIsCompleteLinearAndCountsReusedDisplayColumnsOnce() {
+ MetaCacheSizeEstimator<SchemaCacheKey, SchemaCacheValue> estimator =
+ ExternalCatalogMetaCache.sizeEstimator(SchemaCacheValue.class);
+ SchemaCacheKey key = new SchemaCacheKey(NameMapping.createForTest(3L,
"db1", "wide"));
+ MetaCacheSizeEstimate half = estimator.estimate(key, new
SchemaCacheValue(wideColumns(2_000, 500)));
+ List<Column> columns = wideColumns(4_000, 1_000);
+ SchemaCacheValue value = new SchemaCacheValue(columns);
+ MetaCacheSizeEstimate full = estimator.estimate(key, value);
+
+ Assertions.assertTrue(full.isComplete(), full.getIncompleteReason());
+ // Never below an unbounded walk of the value; above it only by the
objects that columns share.
+ long walked = ReflectiveObjectSizeEstimator.estimateComplete(key,
Integer.MAX_VALUE, 20)
+ + ReflectiveObjectSizeEstimator.estimateComplete(value,
Integer.MAX_VALUE, 20);
+ Assertions.assertTrue(full.getBytes() >= walked, full.getBytes() + " <
" + walked);
+ Assertions.assertTrue(full.getBytes() <= walked * 2, full.getBytes() +
" > 2 * " + walked);
+ double growth = (double) full.getBytes() / half.getBytes();
+ Assertions.assertTrue(growth > 1.8 && growth < 2.2, "doubling the
columns grew the estimate " + growth);
+
+ // A plugin table's display columns reuse its column objects; they
must not be weighed twice. MUTATION:
+ // estimating every element of every column list -> about twice the
weight -> red.
+ MetaCacheSizeEstimator<SchemaCacheKey, PluginDrivenSchemaCacheValue>
pluginEstimator =
+
ExternalCatalogMetaCache.sizeEstimator(PluginDrivenSchemaCacheValue.class);
+ long sameList = pluginEstimator.estimate(key, new
PluginDrivenSchemaCacheValue(columns,
+ Collections.emptyList(), Collections.emptyList(), null, null,
null, columns)).getBytes();
+ long copiedList = pluginEstimator.estimate(key, new
PluginDrivenSchemaCacheValue(columns,
+ Collections.emptyList(), Collections.emptyList(), null, null,
null, new ArrayList<>(columns)))
+ .getBytes();
+ Assertions.assertTrue(copiedList < sameList * 1.1, sameList + " -> " +
copiedList);
+ }
+
+ private static List<Column> wideColumns(int intColumns, int structColumns)
{
+ List<Column> columns = new ArrayList<>(intColumns + structColumns);
+ for (int i = 0; i < intColumns; i++) {
+ Column column = new Column("int_" + i, PrimitiveType.INT);
+ column.setComment("comment " + i);
+ columns.add(column);
+ }
+ for (int i = 0; i < structColumns; i++) {
+ columns.add(new Column("struct_" + i, new StructType(
+ new StructField("a",
ScalarType.createType(PrimitiveType.INT)),
+ new StructField("b", ScalarType.createStringType()))));
+ }
+ return columns;
+ }
+
+ private static final class WideSchemaMetaCache extends
ExternalCatalogMetaCache {
+ private WideSchemaMetaCache(ExecutorService refreshExecutor,
Supplier<SchemaCacheValue> loader) {
+ super("test_engine", refreshExecutor);
+ registerEntry(MetaCacheEntryDef.of(
+ "schema",
+ SchemaCacheKey.class,
+ SchemaCacheValue.class,
+ key -> loader.get(),
+ CacheSpec.of(true, CacheSpec.CACHE_NO_TTL, 10L),
+
MetaCacheEntryInvalidation.forNameMapping(SchemaCacheKey::getNameMapping)));
+ }
+ }
+
private static final class TestExternalMetaCache extends
ExternalCatalogMetaCache {
private TestExternalMetaCache(ExecutorService refreshExecutor) {
super("test_engine", refreshExecutor);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]