Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ public void replayRefreshDb(ExternalObjectLog log) {
}

if (!db.isPresent()) {
// The database object cache can be cold while row-count entries from an earlier
// generation are still resident. Retire the catalog scope because replay cannot
// recover a canonical database id without loading remote metadata.
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(catalog.getId());
LOG.warn("failed to find db when replaying refresh db: {}", log.debugForRefreshDb());
} else {
refreshDbInternal(db.get());
Expand Down Expand Up @@ -168,6 +172,7 @@ public void replayRefreshTable(ExternalObjectLog log) {
}
// See comment in refreshDbInternal for why db and table may be null.
if (!db.isPresent()) {
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(catalog.getId());
LOG.warn("failed to find db when replaying refresh table: {}", log.debugForRefreshTable());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fence cold REFRESH DATABASE replay too. This new table-replay miss branch retires catalog/DB row counts, but replayRefreshDb still cache-lookups the database and only logs/returns when it is absent. Because the DB metadata cache can be evicted while row-count entries remain, a replayed REFRESH DATABASE can leave a stale estimate to be served later. Please invalidate the catalog (or carry/use a canonical DB ID) on the missing-DB branch and add a cold database-cache replay test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8b4749c. When replayRefreshDb cannot recover a cached database object, it now retires the catalog row-count scope without loading remote metadata. Added testColdDatabaseReplayInvalidatesCatalogRowCount.

return;
}
Expand All @@ -178,6 +183,7 @@ public void replayRefreshTable(ExternalObjectLog log) {
table = db.get().getTableForReplay(log.getTableId());
}
if (!table.isPresent()) {
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(catalog.getId(), db.get().getId());
LOG.warn("failed to find table when replaying refresh table: {}", log.debugForRefreshTable());
return;
}
Expand All @@ -195,6 +201,7 @@ public void replayRefreshTable(ExternalObjectLog log) {
HiveExternalMetaCache cache = Env.getCurrentEnv().getExtMetaCacheMgr()
.hive(catalog.getId());
cache.refreshAffectedPartitionsCache((HMSExternalTable) table.get(), modifiedPartNames, newPartNames);
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(table.get());
if (table.get() instanceof HMSExternalTable && log.getLastUpdateTime() > 0) {
((HMSExternalTable) table.get()).setUpdateTime(log.getLastUpdateTime());
}
Expand Down Expand Up @@ -281,6 +288,7 @@ public void refreshPartitions(String catalogName, String dbName, String tableNam
for (String partitionName : partitionNames) {
cache.invalidatePartitionCache(externalTable, partitionName);
}
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(externalTable);
((HMSExternalTable) table).setUpdateTime(updateTime);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@ public void addExternalPartitions(String catalogName, String dbName, String tabl
}

HMSExternalTable hmsTable = (HMSExternalTable) table;
// The metastore mutation has already committed when this event is handled. Fence the
// independent row-count cache even when the local partition cache cannot represent the
// table and this method returns early.
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(hmsTable);
List<Type> partitionColumnTypes;
try {
partitionColumnTypes = hmsTable.getPartitionColumnTypes(MvccUtil.getSnapshotFromContext(hmsTable));
Expand Down Expand Up @@ -1082,6 +1086,7 @@ public void dropExternalPartitions(String catalogName, String dbName, String tab
HMSExternalTable hmsTable = (HMSExternalTable) table;
Env.getCurrentEnv().getExtMetaCacheMgr().hive(catalog.getId())
.dropPartitionsCache(hmsTable, partitionNames, true);
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(hmsTable);
hmsTable.setUpdateTime(updateTime);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public abstract class ExternalCatalog
protected ExternalMetadataOps metadataOps;
protected TransactionManager transactionManager;
protected MetaCache<ExternalDatabase<? extends ExternalTable>> metaCache;
private volatile boolean invalidatingAllMetaCache;
protected ExecutionAuthenticator executionAuthenticator;
protected ThreadPoolExecutor threadPoolWithPreAuth;
// Map lowercase database names to actual remote database names for case-insensitive lookup
Expand Down Expand Up @@ -424,7 +425,8 @@ private void buildMetaCache() {
localDbName -> Optional.ofNullable(
buildDbForInit(null, localDbName, Util.genIdByName(name, localDbName), logType,
true)),
(key, value, cause) -> value.ifPresent(v -> v.resetMetaToUninitialized()));
(key, value, cause) -> value.ifPresent(
v -> v.resetMetaToUninitialized(!invalidatingAllMetaCache)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid scanning the global row-count cache twice for REFRESH CATALOG. onRefreshCache always calls refreshMetaCacheOnly, and this new invalidation is immediately followed by invalidateCatalog when invalidCache=true; the manager's finally performs the same O(N), write-locked catalog scan again. The command default and scheduled refresh both use invalidCache=true, so every normal refresh doubles the pause for row-count readers. Make one owner perform the catalog fence while preserving the single scan for reset-only callers.

}
}

Expand Down Expand Up @@ -656,9 +658,15 @@ public void onRefreshCache(boolean invalidCache) {
* Refresh meta cache only (database level cache), without invalidating catalog level cache.
* This method is safe to call within synchronized block.
*/
private void refreshMetaCacheOnly() {
private synchronized void refreshMetaCacheOnly() {
if (metaCache != null) {
metaCache.invalidateAll();
invalidatingAllMetaCache = true;
try {
metaCache.invalidateAll();
} finally {
invalidatingAllMetaCache = false;
}
Env.getCurrentEnv().getExtMetaCacheMgr().getRowCountCache().invalidateCatalog(id);
}
}

Expand Down Expand Up @@ -1163,10 +1171,24 @@ public void unregisterDatabase(String dbName) {
if (LOG.isDebugEnabled()) {
LOG.debug("unregister database [{}]", dbName);
}
if (isInitialized()) {
metaCache.invalidate(dbName, Util.genIdByName(name, dbName));
// Resolve the canonical database object before removing it from the local metadata cache.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep local DB removal independent of routed cache failures. This new invalidateDb call runs before metaCache.invalidate so it can resolve the numeric ID, but safeInvalidate does not catch a runtime failure from an engine cache. If that happens, control never reaches local removal and the dropped DB remains visible with stale metadata/replay state. Please capture the canonical ID/object first and make metadata removal and row-count fencing finally-safe around engine invalidation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8b4749c. unregisterDatabase captures the canonical database name and numeric ID first, then performs local MetaCache removal in finally so an engine invalidation exception cannot leave the dropped DB visible. The manager-level invalidateDb finally block still fences row counts. Added testUnregisterDatabaseRemovesLocalEntryWhenEngineInvalidationFails.

// The row-count cache can outlive that object and must be invalidated by its numeric id.
boolean catalogInitialized = isInitialized();
Optional<ExternalDatabase<? extends ExternalTable>> db = catalogInitialized
? getDbForReplay(dbName) : Optional.empty();
String localDbName = db.map(ExternalDatabase::getFullName).orElse(dbName);
long dbId = db.map(ExternalDatabase::getId).orElseGet(() -> Util.genIdByName(name, localDbName));
try {
if (db.isPresent()) {
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateDb(getId(), dbId, localDbName);
} else {
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateDb(getId(), dbName);
}
} finally {
if (catalogInitialized) {
metaCache.invalidate(localDbName, dbId);
}
}
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateDb(getId(), dbName);
}

public void registerDatabase(long dbId, String dbName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ public void setRemoteName(String remoteName) {
}

public void resetMetaToUninitialized() {
resetMetaToUninitialized(true);
}

void resetMetaToUninitialized(boolean invalidateRowCountCache) {
if (LOG.isDebugEnabled()) {
LOG.debug("resetToUninitialized db name {}, id {}, isInitializing: {}, initialized: {}",
this.name, this.id, isInitializing, initialized, new Exception());
Expand All @@ -128,7 +132,9 @@ public void resetMetaToUninitialized() {
metaCache.invalidateAll();
}
}
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateDb(extCatalog.getId(), getFullName());
if (invalidateRowCountCache) {
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateDb(extCatalog.getId(), getId(), getFullName());
}
}

public boolean isInitialized() {
Expand Down Expand Up @@ -573,6 +579,9 @@ public void unregisterTable(String tableName) {
// check if the table exists in cache, it not, does return
ExternalTable dorisTable = getTableForReplay(tableName).orElse(null);
if (dorisTable == null) {
// The table object cache is much smaller than the row-count cache. A drop or rename
// must still retire stale row counts when the table object has already been evicted.
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateTable(extCatalog.getId(), getFullName(), tableName);
return;
}
// clear the cache related to this table.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Lock;
Expand Down Expand Up @@ -379,9 +380,13 @@ private Map<String, String> runtimeEffectiveCacheProperties(
}

public void invalidateCatalog(long catalogId) {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateCatalog",
() -> cache.invalidateCatalogEntries(catalogId)));
try {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateCatalog",
() -> cache.invalidateCatalogEntries(catalogId)));
} finally {
rowCountCache.invalidateCatalog(catalogId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Invalidate row counts on every whole-catalog retirement. This call is only reached by invalidateCatalog, while DROP/replay use removeCatalogPermanently and rename uses removeCatalog; neither clears rowCountCache. Because external table IDs are deterministic from names and RowCountKey.equals compares only tableId, recreating the same catalog name can return the dropped catalog's future (and rename-back has the same reuse). Please add a final catalog-scope row-count barrier to those retirement paths.

}
}

public void invalidateCatalogByEngine(long catalogId, String engine) {
Expand Down Expand Up @@ -413,6 +418,7 @@ public void removeCatalog(long catalogId) {
cache, catalogId, "removeCatalog",
() -> cache.invalidateCatalog(catalogId)));
} finally {
rowCountCache.invalidateCatalog(catalogId);
lifecycleLock.unlock();
}
}
Expand Down Expand Up @@ -452,6 +458,7 @@ public void removeCatalogPermanently(long catalogId) {
}
}
} finally {
rowCountCache.invalidateCatalog(catalogId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Apply this fence to tentative-property rollback as well. Legacy validators publish newProps before checkProperties, while lock-free catalog lookup and lazy initialization do not take this lifecycle stripe. A concurrent row-count load can therefore initialize the catalog/client and start against the temporary endpoint. If validation fails, rollbackCatalogProperties below only restores CatalogProperty and routed engine groups; it neither resets candidate-built catalog/DB/table state nor fences row counts, so the rejected target's client/count can remain under unchanged IDs. Reset that legacy state and add the catalog-scope completion fence during rollback, with a latching regression test.

lifecycleLock.unlock();
}
}
Expand All @@ -463,10 +470,12 @@ public void rollbackCatalogProperties(ExternalCatalog catalog, Map<String, Strin
lifecycleLock.lock();
try {
catalog.rollBackCatalogProps(oldProperties);
catalog.resetToUninitialized(false);
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "rollbackCatalogProperties",
() -> cache.invalidateCatalog(catalogId)));
} finally {
rowCountCache.invalidateCatalog(catalogId);
lifecycleLock.unlock();
}
}
Expand All @@ -484,27 +493,88 @@ public void removeCatalogByEngine(long catalogId, String engine) {
}

public void invalidateDb(long catalogId, String dbName) {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateDb", () -> cache.invalidateDb(catalogId, dbName)));
OptionalLong dbId = getCachedDbId(catalogId, dbName);
invalidateDb(catalogId, dbName, dbId);
}

public void invalidateDb(long catalogId, long dbId, String dbName) {
invalidateDb(catalogId, dbName, OptionalLong.of(dbId));
}

private void invalidateDb(long catalogId, String dbName, OptionalLong dbId) {
try {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateDb", () -> cache.invalidateDb(catalogId, dbName)));
} finally {
if (dbId.isPresent()) {
rowCountCache.invalidateDb(catalogId, dbId.getAsLong());
} else {
// The database object cache is smaller than the row-count cache. If the object has
// already been evicted, retire the catalog scope rather than hashing caller spelling.
rowCountCache.invalidateCatalog(catalogId);
}
}
}

public void invalidateTable(long catalogId, String dbName, String tableName) {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateTable",
() -> cache.invalidateTable(catalogId, dbName, tableName)));
Optional<ExternalDatabase<? extends ExternalTable>> db = getCachedDb(catalogId, dbName);
try {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateTable",
() -> cache.invalidateTable(catalogId, dbName, tableName)));
} finally {
invalidateTableRowCount(catalogId, db, tableName);
}
}

public void invalidateTableByEngine(long catalogId, String engine, String dbName, String tableName) {
routeSpecifiedEngine(engine, cache -> safeInvalidate(
cache, catalogId, "invalidateTableByEngine",
() -> cache.invalidateTable(catalogId, dbName, tableName)));
Optional<ExternalDatabase<? extends ExternalTable>> db = getCachedDb(catalogId, dbName);
try {
routeSpecifiedEngine(engine, cache -> safeInvalidate(
cache, catalogId, "invalidateTableByEngine",
() -> cache.invalidateTable(catalogId, dbName, tableName)));
} finally {
invalidateTableRowCount(catalogId, db, tableName);
}
}

public void invalidatePartitions(long catalogId,
String dbName, String tableName, List<String> partitions) {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidatePartitions",
() -> cache.invalidatePartitions(catalogId, dbName, tableName, partitions)));
Optional<ExternalDatabase<? extends ExternalTable>> db = getCachedDb(catalogId, dbName);
try {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidatePartitions",
() -> cache.invalidatePartitions(catalogId, dbName, tableName, partitions)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fence row counts on the engine-only retry. When Hive catches ERR_CACHE_INCONSISTENCY, getHivePartitionValues calls invalidateTableByEngine and retries, but this helper only clears the selected metadata engine. The independent rowCountCache remains publishable, while Hive's fallback row-count estimate reads the same partition/file metadata, so a subsequent getRowCount can return the pre-retry value until TTL. Please fence the table row-count identity here (or in the retry) and add a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8b4749c. invalidateTableByEngine now captures the cached DB/table identity and fences rowCountCache in a finally block, sharing the same conservative table/DB/catalog fallback as the normal invalidation path. Added testEngineSpecificTableInvalidationAlsoFencesRowCount.

} finally {
invalidateTableRowCount(catalogId, db, tableName);
}
}

private void invalidateTableRowCount(long catalogId,
Optional<ExternalDatabase<? extends ExternalTable>> db, String tableName) {
if (db.isPresent()) {
Optional<? extends ExternalTable> table = db.get().getTableForReplay(tableName);
if (table.isPresent()) {
invalidateRowCountCache(table.get());
} else {
rowCountCache.invalidateDb(catalogId, db.get().getId());
}
} else {
rowCountCache.invalidateCatalog(catalogId);
}
}

private OptionalLong getCachedDbId(long catalogId, String dbName) {
Optional<ExternalDatabase<? extends ExternalTable>> db = getCachedDb(catalogId, dbName);
return db.isPresent() ? OptionalLong.of(db.get().getId()) : OptionalLong.empty();
}

private Optional<ExternalDatabase<? extends ExternalTable>> getCachedDb(long catalogId, String dbName) {
CatalogIf<?> catalog = getCatalog(catalogId);
if (!(catalog instanceof ExternalCatalog)) {
return Optional.empty();
}
return ((ExternalCatalog) catalog).getDbForReplay(dbName);
}

public List<CatalogMetaCacheStats> getCatalogCacheStats(long catalogId) {
Expand Down Expand Up @@ -669,15 +739,32 @@ public ExternalRowCountCache getRowCountCache() {
}

public void invalidateTableCache(ExternalTable dorisTable) {
invalidateTable(dorisTable.getCatalog().getId(),
dorisTable.getDbName(),
dorisTable.getName());
long catalogId = dorisTable.getCatalog().getId();
try {
routeCatalogEngines(catalogId, cache -> safeInvalidate(
cache, catalogId, "invalidateTableCache",
() -> cache.invalidateTable(catalogId, dorisTable.getDbName(), dorisTable.getName())));
} finally {
invalidateRowCountCache(dorisTable);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Make this row-count fence reachable from cold TRUNCATE replay. A follower dispatches TruncateTableInfo to HiveMetadataOps.afterTruncateTable, but that callback uses cache-only DB/table lookups and silently does nothing when the table object has been evicted. The table-object cache holds 1,000 entries with no removal listener while row counts retain up to 100,000, so the old count can survive; TRUNCATE preserves the table ID, and subsequent reads reuse it. Add a name-based fallback for an absent table and a cold follower replay test.

}
if (LOG.isDebugEnabled()) {
LOG.debug("invalid table cache for {}.{} in catalog {}", dorisTable.getRemoteDbName(),
dorisTable.getRemoteName(), dorisTable.getCatalog().getName());
}
}

public void invalidateRowCountCache(ExternalTable table) {
rowCountCache.invalidateTable(table.getCatalog().getId(), table.getDb().getId(), table.getId());
}

public void invalidateRowCountCache(long catalogId) {
rowCountCache.invalidateCatalog(catalogId);
}

public void invalidateRowCountCache(long catalogId, long dbId) {
rowCountCache.invalidateDb(catalogId, dbId);
}

public LegacyMetaCacheFactory legacyMetaCacheFactory() {
return legacyMetaCacheFactory;
}
Expand All @@ -698,6 +785,10 @@ void replaceEngineCachesForTest(List<? extends ExternalMetaCache> caches) {
bindCatalogPreparers();
}

void replaceRowCountCacheForTest(ExternalRowCountCache cache) {
rowCountCache = cache;
}

/**
* Fallback implementation of {@link AbstractExternalMetaCache} for engines that do not
* provide dedicated cache entries.
Expand Down
Loading
Loading