github-actions[bot] commented on code in PR #67996:
URL: https://github.com/apache/doris/pull/67996#discussion_r4059855205
##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonConnector.java:
##########
@@ -543,11 +541,15 @@ private Catalog createCatalogFromContext(CatalogContext
catalogContext, String f
try {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
return context.executeAuthenticated(() -> {
+ // PaimonMetaCacheCatalog installs PrivilegedCatalog after the
raw metadata cache.
Catalog catalog = PaimonCatalogProperties.HMS.equals(flavor)
? createHmsCatalog(catalogContext, hmsAuth,
catalogProps.getRaw(),
storageHadoopConfig)
- : CatalogFactory.createCatalog(catalogContext);
- return catalog;
+ :
CatalogFactory.createUnwrappedCatalog(catalogContext,
getClass().getClassLoader());
+ return PaimonMetaCacheCatalog.tryToCreate(catalog, metaCache,
Review Comment:
[P2] Fence this cache-backed catalog publication against close
A statement can retain this connector while `resetToUninitialized()` closes
it. `ensureCatalog()` constructs under the connector monitor, but `close()`
does not take that monitor: it can close `metaCache`, observe the still-null
`catalog`, and return after these two caches have registered but before
`tryToCreate` returns. `ensureCatalog()` then publishes a wrapper backed by
closed caches, so its first table/database lookup throws `Scoped meta cache ...
is closed`, and no later close owns the raw catalog. Please add a connector
lifecycle fence so a successful construction cannot publish after close
(closing the losing raw/decorated catalog exactly once), plus a latch-based
close-versus-construction test.
##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonCacheSizeEstimator.java:
##########
@@ -0,0 +1,163 @@
+// 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.paimon;
+
+import org.apache.doris.connector.cache.JvmSizeUtils;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimate;
+import org.apache.doris.connector.cache.ReflectiveObjectSizeEstimator;
+
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.privilege.PrivilegedFileStoreTable;
+import org.apache.paimon.table.CatalogEnvironment;
+import org.apache.paimon.table.DelegatedFileStoreTable;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.FormatTable;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.iceberg.IcebergTable;
+import org.apache.paimon.table.lance.LanceTable;
+import org.apache.paimon.table.object.ObjectTable;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+
+/**
+ * Retained-size formulas for Paimon table-cache entries.
+ *
+ * <p>The table's shallow size includes references to FileIO, catalog loaders,
and lock factories,
+ * but their graphs are catalog-scoped executable services rather than
entry-owned metadata. Walking
+ * those graphs both double-counts shared state and reaches strongly
encapsulated JDK objects. The
+ * estimator therefore expands only immutable metadata owned by the entry.
+ */
+final class PaimonCacheSizeEstimator {
+ private PaimonCacheSizeEstimator() {
+ }
+
+ static MetaCacheSizeEstimate estimateTable(Identifier key, Table table,
long entryOverheadBytes) {
+ if (table instanceof PrivilegedFileStoreTable) {
+ return MetaCacheSizeEstimate.incomplete(
+ "authorization decorators must be applied outside the
metadata cache");
+ }
+ long bytes = add(entryOverheadBytes,
ReflectiveObjectSizeEstimator.estimateComplete(key));
+ if (table instanceof FileStoreTable) {
+ Set<Object> visited = Collections.newSetFromMap(new
IdentityHashMap<>());
+ bytes = add(bytes, estimateFileStoreTable((FileStoreTable) table,
visited));
+ } else {
+ if (!isSupportedNonFileStoreTable(table)) {
+ return MetaCacheSizeEstimate.incomplete(
+ "unsupported retained graph for " +
table.getClass().getName());
+ }
+ bytes = add(bytes, JvmSizeUtils.instanceSize(table.getClass()));
+ bytes = add(bytes,
ReflectiveObjectSizeEstimator.estimateComplete(table.rowType()));
+ bytes = add(bytes,
ReflectiveObjectSizeEstimator.estimateComplete(table.partitionKeys()));
+ bytes = add(bytes,
ReflectiveObjectSizeEstimator.estimateComplete(table.primaryKeys()));
+ bytes = add(bytes,
ReflectiveObjectSizeEstimator.estimateComplete(table.options()));
+ bytes = add(bytes,
ReflectiveObjectSizeEstimator.estimateComplete(table.comment()));
+ bytes = add(bytes, JvmSizeUtils.stringSize(location(table)));
+ }
+ return MetaCacheSizeEstimate.complete(bytes);
+ }
+
+ private static boolean isSupportedNonFileStoreTable(Table table) {
+ return table instanceof FormatTable
+ || table instanceof ObjectTable
+ || table instanceof LanceTable
+ || table instanceof IcebergTable;
+ }
+
+ private static long estimateFileStoreTable(FileStoreTable table,
Set<Object> visited) {
+ if (!visited.add(table)) {
+ return 0L;
+ }
+ long bytes = JvmSizeUtils.instanceSize(table.getClass());
+ if (table instanceof FallbackReadFileStoreTable) {
+ FallbackReadFileStoreTable fallback = (FallbackReadFileStoreTable)
table;
+ bytes = add(bytes, estimateFileStoreTable(fallback.wrapped(),
visited));
+ return add(bytes, estimateFileStoreTable(fallback.fallback(),
visited));
+ }
+ if (table instanceof DelegatedFileStoreTable) {
+ return add(bytes, estimateFileStoreTable(
+ ((DelegatedFileStoreTable) table).wrapped(), visited));
+ }
+ bytes = add(bytes, estimateCompleteOnce(table.schema(), visited));
+ bytes = add(bytes, estimatePath(table.location(), visited));
+ return add(bytes,
estimateCatalogEnvironment(table.catalogEnvironment(), visited));
Review Comment:
[P2] Account for the lazily retained FileStore before admission
This marks a normal `FileStoreTable` complete while its entry-owned graph
can still grow after the one-time weight reservation. In Paimon 1.3.1, both
`AppendOnlyFileStoreTable` and `PrimaryKeyFileStoreTable` start with `lazyStore
== null`; the first ordinary `latestSnapshot()` call from
`beginQuerySnapshot()` invokes `store()` and retains a newly allocated
`AppendOnlyFileStore`/`KeyValueFileStore` plus its managers and factories on
the cached raw table. Under an enclosing weight limit the three explicit SDK
caches are skipped, but this lazy field is still populated and
`ScopedMetaCache` never reweighs reads, so many cached tables can exceed the
configured hard byte budget. Please initialize and charge this retained graph
before returning `complete`, reject the entry under weight governance, or cache
an immutable descriptor. Extend the governed real-table test past `getTable()`
into `latestSnapshot()`/`newScan()` to cover post-admission growth.
##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalog.java:
##########
@@ -0,0 +1,410 @@
+// 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.paimon;
+
+import org.apache.doris.connector.cache.CacheSpec;
+import org.apache.doris.connector.cache.CatalogMetaCache;
+import org.apache.doris.connector.cache.JvmSizeUtils;
+import org.apache.doris.connector.cache.MetaCache;
+import org.apache.doris.connector.cache.MetaCacheDefinition;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimators;
+import org.apache.doris.connector.cache.ScopePath;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogLoader;
+import org.apache.paimon.catalog.Database;
+import org.apache.paimon.catalog.DelegateCatalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.catalog.PropertyChange;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.options.MemorySize;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.privilege.PrivilegedCatalog;
+import org.apache.paimon.schema.SchemaChange;
+import
org.apache.paimon.shade.caffeine2.com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.system.SystemTableLoader;
+import org.apache.paimon.utils.SegmentsCache;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.LongSupplier;
+
+/**
+ * Doris-owned replacement for Paimon's {@code CachingCatalog}. Table and
database entries live in
+ * {@link CatalogMetaCache}, so a Doris catalog/database/table invalidation
fences every matching
+ * in-flight load and cached value.
+ *
+ * <p>The cache retains raw table metadata. Paimon's privilege catalog is
applied outside this
+ * wrapper so every lookup receives a fresh checker instead of caching one
authorization snapshot.
+ *
+ * <p>The user's {@code paimon.cache-enabled} and access/write expiry settings
remain authoritative.
+ * The Paimon SDK wrapper itself is disabled because a second hidden table
cache cannot participate
+ * in Doris invalidation. Under a Doris weight budget, mutable SDK
snapshot/stats/manifest caches are
+ * not attached: their post-publication growth cannot be reweighed by the
enclosing budget.
+ */
+final class PaimonMetaCacheCatalog extends DelegateCatalog {
+
+ private static final int DATABASE_CACHE_CAPACITY = 100;
+ static final long TABLE_ENTRY_OVERHEAD_BYTES = JvmSizeUtils.saturatedAdd(
+ JvmSizeUtils.instanceSize(ExpiringValue.class),
JvmSizeUtils.instanceSize(AtomicLong.class));
+
+ private final CatalogMetaCache metaCache;
+ private final MetaCache<Identifier, ExpiringValue<Table>> tableCache;
+ private final MetaCache<String, ExpiringValue<Database>> databaseCache;
+ private final SegmentsCache<Path> manifestCache;
+ private final long tableExpireAfterAccessNanos;
+ private final long databaseExpireAfterAccessNanos;
+ private final long expireAfterWriteNanos;
+ private final int snapshotMaxNumPerTable;
+ private final boolean attachSdkCaches;
+ private final LongSupplier nanoTime;
+ private final BiConsumer<String, Object> cacheMissObserver;
+
+ static Catalog tryToCreate(Catalog wrapped, CatalogMetaCache metaCache,
int tableCacheMaxSize,
+ long tableCacheTtlSecond, Options catalogOptions, boolean
cacheEnabled,
+ boolean hasEnclosingWeightLimit) {
+ return tryToCreate(wrapped, metaCache, tableCacheMaxSize,
tableCacheTtlSecond,
+ catalogOptions, cacheEnabled, hasEnclosingWeightLimit,
+ catalog -> PrivilegedCatalog.tryToCreate(catalog,
catalogOptions));
+ }
+
+ static Catalog tryToCreate(Catalog wrapped, CatalogMetaCache metaCache,
int tableCacheMaxSize,
+ long tableCacheTtlSecond, Options catalogOptions, boolean
cacheEnabled,
+ boolean hasEnclosingWeightLimit, Function<Catalog, Catalog>
decorator) {
+ PaimonMetaCacheCatalog cached = null;
+ try {
+ cached = new PaimonMetaCacheCatalog(wrapped, metaCache,
tableCacheMaxSize,
+ tableCacheTtlSecond, catalogOptions, cacheEnabled,
hasEnclosingWeightLimit,
+ System::nanoTime, (name, key) -> { });
+ return decorator.apply(cached);
+ } catch (RuntimeException | Error throwable) {
+ if (cached != null) {
+ try {
+ cached.unregisterCaches();
+ } catch (RuntimeException | Error rollbackFailure) {
+ throwable.addSuppressed(rollbackFailure);
+ }
+ }
+ try {
+ wrapped.close();
+ } catch (Exception | Error closeFailure) {
+ throwable.addSuppressed(closeFailure);
+ }
+ throw throwable;
+ }
+ }
+
+ PaimonMetaCacheCatalog(Catalog wrapped, CatalogMetaCache metaCache, int
tableCacheMaxSize,
+ long tableCacheTtlSecond, Options catalogOptions, boolean
hasEnclosingWeightLimit,
+ LongSupplier nanoTime) {
+ this(wrapped, metaCache, tableCacheMaxSize, tableCacheTtlSecond,
catalogOptions,
+ true, hasEnclosingWeightLimit, nanoTime, (name, key) -> { });
+ }
+
+ PaimonMetaCacheCatalog(Catalog wrapped, CatalogMetaCache metaCache, int
tableCacheMaxSize,
+ long tableCacheTtlSecond, Options catalogOptions, boolean
hasEnclosingWeightLimit,
+ LongSupplier nanoTime, BiConsumer<String, Object>
cacheMissObserver) {
+ this(wrapped, metaCache, tableCacheMaxSize, tableCacheTtlSecond,
catalogOptions,
+ true, hasEnclosingWeightLimit, nanoTime, cacheMissObserver);
+ }
+
+ private PaimonMetaCacheCatalog(Catalog wrapped, CatalogMetaCache
metaCache, int tableCacheMaxSize,
+ long tableCacheTtlSecond, Options catalogOptions, boolean
cacheEnabled,
+ boolean hasEnclosingWeightLimit, LongSupplier nanoTime,
+ BiConsumer<String, Object> cacheMissObserver) {
+ super(wrapped);
+ this.metaCache = metaCache;
+ this.nanoTime = nanoTime;
+ this.cacheMissObserver = cacheMissObserver;
+
+ Duration expireAfterAccess = cacheEnabled
+ ? catalogOptions.get(CatalogOptions.CACHE_EXPIRE_AFTER_ACCESS)
: null;
+ Duration expireAfterWrite = cacheEnabled
+ ? catalogOptions.get(CatalogOptions.CACHE_EXPIRE_AFTER_WRITE)
: null;
+ if (cacheEnabled) {
+ requirePositive(expireAfterAccess,
CatalogOptions.CACHE_EXPIRE_AFTER_ACCESS.key());
+ requirePositive(expireAfterWrite,
CatalogOptions.CACHE_EXPIRE_AFTER_WRITE.key());
+ }
+ long paimonAccessNanos = cacheEnabled ?
saturatedNanos(expireAfterAccess) : Long.MAX_VALUE;
+ this.tableExpireAfterAccessNanos = cacheEnabled && tableCacheTtlSecond
> 0
+ ? Math.min(paimonAccessNanos,
saturatedNanos(Duration.ofSeconds(tableCacheTtlSecond)))
+ : paimonAccessNanos;
+ this.databaseExpireAfterAccessNanos = paimonAccessNanos;
+ this.expireAfterWriteNanos = cacheEnabled ?
saturatedNanos(expireAfterWrite) : Long.MAX_VALUE;
+ this.attachSdkCaches = cacheEnabled && !hasEnclosingWeightLimit;
+ this.manifestCache = attachSdkCaches ?
buildManifestCache(catalogOptions) : null;
+ this.snapshotMaxNumPerTable = attachSdkCaches
+ ?
catalogOptions.get(CatalogOptions.CACHE_SNAPSHOT_MAX_NUM_PER_TABLE) : 0;
+
+ CacheSpec tableSpec = CacheSpec.of(cacheEnabled,
+ cacheEnabled && tableCacheTtlSecond > 0
+ ? CacheSpec.CACHE_NO_TTL :
CacheSpec.CACHE_TTL_DISABLE_CACHE,
+ tableCacheMaxSize);
+ MetaCache<Identifier, ExpiringValue<Table>> createdTableCache =
metaCache.create(
+ MetaCacheDefinition
+ .<Identifier,
ExpiringValue<Table>>builder("paimon-table", tableSpec,
+ id -> ScopePath.table(id.getDatabaseName(),
id.getTableName()))
+ .sizeEstimator((id, value) ->
PaimonCacheSizeEstimator.estimateTable(
+ id, value.value, TABLE_ENTRY_OVERHEAD_BYTES))
+ .build());
+ try {
+ CacheSpec dbSpec = CacheSpec.of(cacheEnabled, cacheEnabled
+ ? CacheSpec.CACHE_NO_TTL :
CacheSpec.CACHE_TTL_DISABLE_CACHE, DATABASE_CACHE_CAPACITY);
+ this.databaseCache = metaCache.create(MetaCacheDefinition
+ .<String,
ExpiringValue<Database>>builder("paimon-database", dbSpec, ScopePath::database)
+ .sizeEstimator(MetaCacheSizeEstimators.reflective())
+ .build());
+ } catch (RuntimeException | Error throwable) {
+ try {
+ metaCache.remove(createdTableCache);
+ } catch (RuntimeException | Error rollbackFailure) {
+ throwable.addSuppressed(rollbackFailure);
+ }
+ throw throwable;
+ }
+ this.tableCache = createdTableCache;
+ }
+
+ @Override
+ public Table getTable(Identifier identifier) throws TableNotExistException
{
+ if (identifier.isSystemTable()) {
+ Identifier origin = new Identifier(identifier.getDatabaseName(),
identifier.getTableName(),
+ identifier.getBranchName(), null);
+ Table originTable = getTable(origin);
+ if (!(originTable instanceof FileStoreTable)) {
+ return super.getTable(identifier);
+ }
+ Table systemTable =
SystemTableLoader.load(identifier.getSystemTableName(),
+ (FileStoreTable) originTable);
+ if (systemTable == null) {
+ throw new TableNotExistException(identifier);
+ }
+ return systemTable;
+ }
+
+ while (true) {
+ ExpiringValue<Table> cached = tableCache.getIfPresent(identifier);
+ if (cached == null) {
+ cacheMissObserver.accept("table", identifier);
+ try {
+ cached = tableCache.get(identifier, ignored -> {
+ try {
+ Table loaded =
attachPerTableCaches(super.getTable(identifier));
+ return new ExpiringValue<>(loaded,
nanoTime.getAsLong());
+ } catch (TableNotExistException e) {
+ throw new CatalogLoadException(e);
+ }
+ });
+ } catch (CatalogLoadException e) {
+ throw (TableNotExistException) e.getCause();
+ }
+ }
+ if (cached.tryAccess(nanoTime.getAsLong(),
tableExpireAfterAccessNanos,
Review Comment:
[P2] Return a freshly loaded value before re-expiring it
This unconditional recheck can make a cold lookup nonterminating for a valid
short positive expiry. For example, with `cache.expire-after-access=1 ns`, the
loader records one `nanoTime`, publication completes, and this second clock
read makes `tryAccess` reject the just-loaded value. The CAS removes it and
`while (true)` repeats the remote load forever; the database path has the same
loop, and disabled/rejected physical admission cannot break it. Please
distinguish the current shared load's fresh result from a pre-existing miss-gap
result (or age it from publication) so this caller returns once, while
retaining the resolved miss-gap revalidation. A deterministic advancing-clock
test should cover both table and database lookups.
--
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]