Skip to content

[Bug] HMS incremental events have multiple metadata cache consistency gaps #65750

Open

Description

@wenzhenghu

Status after connector SPI merge (#64304)

Updated against master after connector SPI PR #64304 was merged and PR #65126 was rebased on top of it.

Item Current status after #64304 Tracking decision
1. Rename cancelled by load-through existence check Fixed by the current revision of #65126, pending merge Keep in #65126
2. Partition event stale-load/lower-cache gap Fixed structurally by #64304 Partition events now call connector-owned partition invalidation; partition-name, partition-object, and file-listing caches are invalidated, and the connector cache generation fence rejects pre-event manual loads
3. CREATE event bypasses visibility filters Open; still present on current master and #65126 Keep in this issue
4. DROP_PARTITION writes a database deletion to ExternalMetaIdMgr Obsolete/fixed by #64304 The legacy event ID-mapping payload path was removed; event sync now persists only the cursor
5. Database event ID uses the remote rather than canonical local name Open; still present on current master and #65126 Keep in this issue
6. Database event publishes a non-canonical local key Open; still present on current master and #65126 Keep in this issue
7. Database/table CREATE, DROP, and RENAME events do not invalidate connector-owned caches Open; introduced by the cache-ownership split in #64304 and now part of master baseline Track here rather than block #65126, provided #65126 does not claim connector-cache convergence

No top-level issue is closed by this update because items 3, 5, 6, and 7 remain open.

Search before asking

I searched existing Apache Doris issues and did not find an issue covering these HMS incremental-event cache consistency problems.

Version

Current status verified on Apache Doris master at af6dcff905114d7e05a6101496f5a629d8831b64, after connector SPI PR #64304 was merged. The original report was collected at 3a75387e61388bd886eeef38b794d5ed4eb298bf.

The remaining open items are master-baseline issues and are not introduced by #65126. Item 1 is fixed by #65126 pending merge; items 2 and 4 were fixed or eliminated by #64304.

What's Wrong?

The original report identified multiple independent correctness gaps in HMS incremental event handling. The status table above reflects the current code after #64304.

1. ALTER TABLE/ALTER DATABASE rename can be incorrectly cancelled by a load-through "local existence" check

Status: Fixed by the current revision of #65126 (pending merge).

AlterTableEvent.processRename() calls externalTableExistInLocal() before applying the rename:

private void processRename() throws DdlException {
if (!isRename) {
return;
}
boolean hasExist = Env.getCurrentEnv().getCatalogMgr()
.externalTableExistInLocal(tableAfter.getDbName(), tableAfter.getTableName(), catalogName);
if (hasExist) {
logInfo("AlterExternalTable canceled,because tableAfter has exist, "
+ "catalogName:[{}],dbName:[{}],tableName:[{}]",
catalogName, dbName, tableAfter.getTableName());
return;
}
Env.getCurrentEnv().getCatalogMgr()
.unregisterExternalTable(tableBefore.getDbName(), tableBefore.getTableName(), catalogName, true);
Env.getCurrentEnv().getCatalogMgr()
.registerExternalTableFromEvent(
tableAfter.getDbName(), tableAfter.getTableName(), catalogName, eventTime, true);

However, HMSExternalCatalog.tableExistInLocal() eventually calls the normal load-through table lookup rather than a cache-only lookup:

@Override
public boolean tableExistInLocal(String dbName, String tblName) {
makeSureInitialized();
HMSExternalDatabase hmsExternalDatabase = (HMSExternalDatabase) getDbNullable(dbName);
if (hmsExternalDatabase == null) {
return false;
}
return hmsExternalDatabase.getTable(tblName).isPresent();

After HMS has renamed old_table to new_table, querying new_table from this check can load it from HMS and return true. The event is then cancelled, and cached state for old_table may remain. AlterDatabaseEvent.processRename() has the same structural problem through catalog.getDbNullable(dbAfter.getName()), although database rename is less commonly reachable in standard Hive.

The current revision of #65126 removes the load-through target-existence checks and makes both rename handlers converge local state by unregistering the old identity and registering the new identity. Its focused FE unit tests cover cold and already-hot rename targets.

2. ADD/DROP PARTITION events do not fence an in-flight partition-values load, and DROP may skip lower-level cache invalidation

Current status: Fixed structurally by #64304. The details below describe the former legacy implementation.

Both addPartitionsCache() and dropPartitionsCache() return immediately when the partition-values entry is absent:

private void addPartitionsCache(NameMapping nameMapping,
List<String> partitionNames,
List<Type> partitionColumnTypes) {
long catalogId = nameMapping.getCtlId();
MetaCacheEntry<PartitionValueCacheKey, HivePartitionValues> partitionValuesEntry =
partitionValuesEntryIfInitialized(catalogId);
if (partitionValuesEntry == null) {
return;
}
PartitionValueCacheKey key = new PartitionValueCacheKey(nameMapping, partitionColumnTypes);
HivePartitionValues partitionValues = partitionValuesEntry.getIfPresent(key);
if (partitionValues == null) {
return;
}
HivePartitionValues copy = partitionValues.copy();
Map<String, PartitionItem> nameToPartitionItem = copy.getNameToPartitionItem();
Map<String, List<String>> nameToPartitionValues = copy.getNameToPartitionValues();
HMSExternalCatalog catalog = hmsCatalog(catalogId);
String localTblName = nameMapping.getLocalTblName();
for (String partitionName : partitionNames) {
if (nameToPartitionItem.containsKey(partitionName)) {
LOG.info("addPartitionsCache partitionName:[{}] has exist in table:[{}]",
partitionName, localTblName);
continue;
}
ListPartitionItem listPartitionItem = toListPartitionItem(partitionName, key.types, catalog.getName());
nameToPartitionItem.put(partitionName, listPartitionItem);
nameToPartitionValues.put(partitionName, HiveUtil.toPartitionValues(partitionName));
}
copy.rebuildSortedPartitionRanges();
HivePartitionValues partitionValuesCur = partitionValuesEntry.getIfPresent(key);
if (partitionValuesCur == partitionValues) {
partitionValuesEntry.put(key, copy);
}
}

private void dropPartitionsCache(ExternalTable dorisTable,
List<String> partitionNames,
boolean invalidPartitionCache) {
NameMapping nameMapping = dorisTable.getOrBuildNameMapping();
long catalogId = nameMapping.getCtlId();
MetaCacheEntry<PartitionValueCacheKey, HivePartitionValues> partitionValuesEntry =
partitionValuesEntryIfInitialized(catalogId);
if (partitionValuesEntry == null) {
return;
}
PartitionValueCacheKey key = new PartitionValueCacheKey(nameMapping, null);
HivePartitionValues partitionValues = partitionValuesEntry.getIfPresent(key);
if (partitionValues == null) {
return;
}
HivePartitionValues copy = partitionValues.copy();
Map<String, PartitionItem> nameToPartitionItem = copy.getNameToPartitionItem();
Map<String, List<String>> nameToPartitionValues = copy.getNameToPartitionValues();
for (String partitionName : partitionNames) {
if (!nameToPartitionItem.containsKey(partitionName)) {
LOG.info("dropPartitionsCache partitionName:[{}] not exist in table:[{}]",
partitionName, nameMapping.getFullLocalName());
continue;
}
nameToPartitionItem.remove(partitionName);
nameToPartitionValues.remove(partitionName);
if (invalidPartitionCache) {
invalidatePartitionCache(nameMapping, partitionName);
}
}
copy.rebuildSortedPartitionRanges();
HivePartitionValues partitionValuesCur = partitionValuesEntry.getIfPresent(key);
if (partitionValuesCur == partitionValues) {
partitionValuesEntry.put(key, copy);
}
}

This causes two problems:

  • If a partition-values load started before the event, the event does not invalidate/bump the entry, so the pre-event snapshot may still be published afterward.
  • If the partition-values entry has been evicted while partition metadata or file listings remain cached, a DROP PARTITION event returns before invalidating those lower-level entries.

The second case can expose stale data when a partition name is later recreated with a different path or files. It also prevents the cache-miss fallback added by #65334 from being reached through this DROP event path.

3. Incremental CREATE_DATABASE/CREATE_TABLE events can bypass include/exclude visibility filters

Full names loading applies include_database_list, exclude_database_list, and include_table_list:

private List<Pair<String, String>> getFilteredDatabaseNames() {
List<String> allDatabases = Lists.newArrayList(listDatabaseNames());
allDatabases.remove(InfoSchemaDb.DATABASE_NAME);
allDatabases.add(InfoSchemaDb.DATABASE_NAME);
allDatabases.remove(MysqlDb.DATABASE_NAME);
allDatabases.add(MysqlDb.DATABASE_NAME);
Map<String, Boolean> includeDatabaseMap = getIncludeDatabaseMap();
Map<String, Boolean> excludeDatabaseMap = getExcludeDatabaseMap();
lowerCaseToDatabaseName.clear();
List<Pair<String, String>> remoteToLocalPairs = Lists.newArrayList();
allDatabases = allDatabases.stream().filter(dbName -> {
if (!dbName.equals(InfoSchemaDb.DATABASE_NAME) && !dbName.equals(MysqlDb.DATABASE_NAME)) {
// Exclude database map take effect with higher priority over include database map
if (!excludeDatabaseMap.isEmpty() && excludeDatabaseMap.containsKey(dbName)) {
return false;
}
if (!includeDatabaseMap.isEmpty() && !includeDatabaseMap.containsKey(dbName)) {
return false;
}
}
return true;
}).collect(Collectors.toList());
for (String remoteDbName : allDatabases) {
String localDbName = fromRemoteDatabaseName(remoteDbName);
// Populate lowercase mapping for case-insensitive lookups
lowerCaseToDatabaseName.put(remoteDbName.toLowerCase(), remoteDbName);
// Apply lower_case_database_names mode to local name
int dbNameMode = getLowerCaseDatabaseNames();
if (dbNameMode == 1) {
localDbName = localDbName.toLowerCase();
} else if (dbNameMode == 2) {
// Mode 2: preserve original remote case for display
localDbName = remoteDbName;
}
remoteToLocalPairs.add(Pair.of(remoteDbName, localDbName));
}
// Check for conflicts when lower_case_meta_names = true or lower_case_database_names = 2
if (Boolean.parseBoolean(getLowerCaseMetaNames()) || getLowerCaseDatabaseNames() == 2) {
// Map to track lowercase local names and their corresponding remote names
Map<String, List<String>> lowerCaseToRemoteNames = Maps.newHashMap();
// Collect lowercased local names and their remote counterparts
for (Pair<String, String> pair : remoteToLocalPairs) {
String lowerCaseLocalName = pair.second.toLowerCase();
lowerCaseToRemoteNames.computeIfAbsent(lowerCaseLocalName, k -> Lists.newArrayList()).add(pair.first);
}
// Identify conflicts: multiple remote names mapping to the same lowercase local name
List<String> conflicts = lowerCaseToRemoteNames.values().stream()
.filter(remoteNames -> remoteNames.size() > 1) // Conflict: more than one remote name
.flatMap(List::stream) // Collect all conflicting remote names
.collect(Collectors.toList());
// Throw exception if conflicts are found
if (!conflicts.isEmpty()) {
throw new RuntimeException(String.format(
FOUND_CONFLICTING + " database names under case-insensitive conditions. "
+ "Conflicting remote database names: %s in catalog %s. "
+ "Please use meta_names_mapping to handle name mapping.",
String.join(", ", conflicts), name));
}

* @return names of tables in specified database, filtered by include_table_list if configured
*/
public final List<String> listTableNames(SessionContext ctx, String dbName) {
makeSureInitialized();
Map<String, List<String>> includeTableMap = getIncludeTableMap();
if (includeTableMap.containsKey(dbName) && !includeTableMap.get(dbName).isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("get table list from include map. catalog: {}, db: {}, tables: {}",
name, dbName, includeTableMap.get(dbName));
}
return includeTableMap.get(dbName);
}
return listTableNamesFromRemote(ctx, dbName);
}

The incremental register paths directly update the legacy metadata cache without applying the same visibility policy:

@Override
public void registerDatabase(long dbId, String dbName) {
if (LOG.isDebugEnabled()) {
LOG.debug("create database [{}]", dbName);
}
ExternalDatabase<? extends ExternalTable> db = buildDbForInit(dbName, null, dbId, logType, false);
if (isInitialized()) {
metaCache.updateCache(db.getRemoteName(), db.getFullName(), db,
Util.genIdByName(name, db.getFullName()));
}

// Only used for sync hive metastore event
@Override
public boolean registerTable(TableIf tableIf) {
makeSureInitialized();
String tableName = tableIf.getName();
if (LOG.isDebugEnabled()) {
LOG.debug("create table [{}]", tableName);
}
if (isInitialized()) {
String localName = extCatalog.fromRemoteTableName(this.remoteName, tableName);
metaCache.updateCache(tableName, localName, (T) tableIf,
Util.genIdByName(extCatalog.getName(), name, localName));
lowerCaseToTableName.put(tableName.toLowerCase(), tableName);
}
setLastUpdateTime(System.currentTimeMillis());

When a names cache is already hot, an HMS CREATE event can therefore add a database/table that should remain hidden. The object can subsequently become queryable through the cached name.

4. DROP_PARTITION records a DATABASE deletion in ExternalMetaIdMgr

Current status: Obsolete/fixed by #64304. The legacy per-event ID-mapping payload path no longer exists.

DropPartitionEvent.transferToMetaIdMappings() uses META_OBJECT_TYPE_DATABASE instead of META_OBJECT_TYPE_PARTITION:

@Override
protected List<MetaIdMappingsLog.MetaIdMapping> transferToMetaIdMappings() {
List<MetaIdMappingsLog.MetaIdMapping> metaIdMappings = Lists.newArrayList();
for (String partitionName : this.getAllPartitionNames()) {
MetaIdMappingsLog.MetaIdMapping metaIdMapping = new MetaIdMappingsLog.MetaIdMapping(
MetaIdMappingsLog.OPERATION_TYPE_DELETE,
MetaIdMappingsLog.META_OBJECT_TYPE_DATABASE,
dbName, tblName, partitionName);
metaIdMappings.add(metaIdMapping);
}
return ImmutableList.copyOf(metaIdMappings);

ExternalMetaIdMgr interprets this as a request to remove the entire database mapping subtree:

private static void handleDelMetaIdMapping(MetaIdMappingsLog.MetaIdMapping mapping,
CtlMetaIdMgr ctlMetaIdMgr,
MetaIdMappingsLog.MetaObjectType objType) {
TblMetaIdMgr tblMetaIdMgr;
DbMetaIdMgr dbMetaIdMgr;
switch (objType) {
case DATABASE:
ctlMetaIdMgr.dbNameToMgr.remove(mapping.getDbName());
break;
case TABLE:
dbMetaIdMgr = ctlMetaIdMgr.dbNameToMgr.get(mapping.getDbName());
if (dbMetaIdMgr != null) {
dbMetaIdMgr.tblNameToMgr.remove(mapping.getTblName());
}
break;
case PARTITION:
dbMetaIdMgr = ctlMetaIdMgr.dbNameToMgr.get(mapping.getDbName());
if (dbMetaIdMgr != null) {
tblMetaIdMgr = dbMetaIdMgr.tblNameToMgr.get(mapping.getTblName());
if (tblMetaIdMgr != null) {
tblMetaIdMgr.partitionNameToMgr.remove(mapping.getPartitionName());
}
}

Current production use of the ExternalMetaIdMgr lookup APIs is limited, so the immediate query impact appears limited, but the persisted/replayed external metadata ID state is incorrect.

5. ALTER_DATABASE rename can generate an ID from the remote name instead of the canonical local name

Status: Open. This is present on the master baseline and is not introduced by #65126.

CatalogMgr.registerExternalDatabaseFromEvent() generates the event-created database ID directly from the incoming remote dbName:

public void registerExternalDatabaseFromEvent(String dbName, String catalogName)
throws DdlException {
CatalogIf catalog = nameToCatalog.get(catalogName);
if (catalog == null) {
throw new DdlException("No catalog found with name: " + catalogName);
}
if (!(catalog instanceof ExternalCatalog)) {
throw new DdlException("Only support create ExternalCatalog databases");
}
HMSExternalCatalog hmsCatalog = (HMSExternalCatalog) catalog;
long dbId = Util.genIdByName(catalogName, dbName);
// -1L means it will be dropped later, ignore
if (dbId == ExternalMetaIdMgr.META_ID_FOR_NOT_EXISTS) {
return;
}
hmsCatalog.registerDatabase(dbId, dbName);

Normal external-database object loading instead generates the ID from the canonical localDbName:

OptionalLong.of(Config.external_cache_expire_time_seconds_after_access),
OptionalLong.of(Config.external_cache_refresh_time_minutes * 60L),
Math.max(Config.max_meta_object_cache_num, 1),
ignored -> getFilteredDatabaseNames(),
localDbName -> Optional.ofNullable(
buildDbForInit(null, localDbName, Util.genIdByName(name, localDbName), logType,
true)),
(key, value, cause) -> value.ifPresent(v -> v.resetMetaToUninitialized()));

When lower_case_database_names = 1, or more generally whenever the incoming remote name differs from its canonical local cache key, the same database can therefore have two deterministic IDs:

  • Immediately after the event: ID(remote name) -> canonical local name.
  • After cache reset or normal reload: ID(canonical local name) -> canonical local name.

This makes the database ID and the local ID-to-name index unstable across refresh/reload and can break ID-based lookup consistency.

What You Expected?

  • Rename events should inspect only locally published cache state and should always remove the old local name. This is fixed by the current revision of [refactor](meta cache) Refactor external metadata cache with MetaCacheEntry #65126.
  • Partition events should prevent pre-event snapshots from being published and should invalidate partition/file caches even when partition-values cache state is cold.
  • Incremental create events should apply the same visibility rules as full names loading.
  • DROP_PARTITION should delete only the corresponding partition metadata-ID mapping.
  • Incremental database registration should derive the database ID from the same canonical local name used by normal object loading.
  • Incremental database registration should also publish names/object/ID navigation state under that same canonical local cache key.

How to Reproduce?

Rename cancellation (fixed by the current revision of #65126)

  1. Enable HMS incremental event synchronization.
  2. Ensure the target name is not locally cached.
  3. Rename old_table to new_table in Hive.
  4. Process the ALTER_TABLE event.
  5. On the affected baseline, the existence check loads new_table from HMS and cancels the event; cached old_table state may remain.

Partition cache

  1. Cache partition metadata/file listings for p=1.
  2. Evict or invalidate only the table's partition-values entry, or block an in-flight partition-values load.
  3. Drop p=1 through HMS and process the DROP_PARTITION event.
  4. Recreate p=1 with a different location/files.
  5. The old partition metadata or file listing may still be reused.

Visibility filter

  1. Create an HMS catalog with an include/exclude database filter or include_table_list.
  2. Warm the corresponding names cache.
  3. Create a filtered-out database/table directly in Hive.
  4. Process the HMS CREATE event.
  5. Observe that the filtered object is inserted into the cached names and becomes visible/queryable.

External metadata ID mapping

  1. Add a database/table/partition mapping to ExternalMetaIdMgr.
  2. Process the partition's DROP_PARTITION mapping.
  3. Observe that the database mapping subtree is removed instead of only the partition mapping.

Database ID stability

  1. Configure an HMS catalog with lower_case_database_names = 1.
  2. Process an ALTER_DATABASE rename event whose new remote name contains uppercase characters.
  3. Observe that registerExternalDatabaseFromEvent() generates the new ID from the remote mixed-case name while the local cache key is lowercase.
  4. Reset or reload the catalog so the normal database-object loader runs.
  5. Observe that the reloaded database ID is generated from the lowercase local name and differs from the event-created ID.

Database local-key publication

  1. Configure an HMS catalog with lower_case_database_names = 1.
  2. Process a CREATE_DATABASE or ALTER_DATABASE rename event whose remote name contains uppercase characters.
  3. Observe that the event-created database is published under the raw-case local key returned by buildDbForInit(remoteDbName, null, ...) rather than the canonical lowercase local key.
  4. Perform a normal database lookup that first canonicalizes the name to lowercase.
  5. Observe that the event-published database state no longer follows the same local-key convention as ordinary lookup and reload paths.

Suggested Fixes

  1. Fixed in the current revision of [refactor](meta cache) Refactor external metadata cache with MetaCacheEntry #65126: remove the load-through rename target-existence checks and always converge the old/new local identities.
  2. For ADD/DROP PARTITION, invalidate or bump the partition-values key when it is cold/loading. DROP should unconditionally invalidate partition metadata/file cache entries for each dropped partition before optionally updating a hot partition-values snapshot.
  3. Reuse a shared database/table visibility predicate in both full names loading and incremental registration.
  4. Change the DROP_PARTITION mapping type to META_OBJECT_TYPE_PARTITION and add an event mapping unit test.
  5. Resolve the canonical local database name with a cache-only helper, and use it consistently for event registration, invalidation, and Util.genIdByName(). Add mode-1 coverage that asserts the event-created ID equals the normal reload ID.
  6. Reuse that same canonical local database name when publishing the event-created database into the names snapshot, object cache, and dbIdToName map. Add mode-1 coverage that asserts event-created cache keys match the normal lookup/reload convention.

The remaining four open fixes can be implemented as separate PRs if preferred.

6. Incremental database event registration can publish a non-canonical local cache key

Status: Open. This is present on the master baseline and is not introduced by #65126.

HMSExternalCatalog.registerDatabase() builds the event-created database object through buildDbForInit(remoteDbName, null, ...):

return false;
}
return hmsExternalDatabase.getTable(tblName).isPresent();
}
public HMSCachedClient getClient() {
makeSureInitialized();
return ((HiveMetadataOps) metadataOps).getClient();
}

However, ExternalCatalog.buildDbForInit() only applies fromRemoteDatabaseName(remoteDbName) when localDbName == null and does not also canonicalize the local cache key under lower_case_database_names = 1:

// Handle "Found conflicting" exception explicitly
if (e.getMessage().contains(FOUND_CONFLICTING)) {
LOG.error(e.getMessage());
throw e; // Rethrow to let the caller handle this critical issue
} else {
// Any errors other than name conflicts, we default to not finding the database

Normal database lookup does canonicalize the local key first:

// information_schema db name is case-insensitive.
// So, we first convert it to standard database name.
if (dbName.equalsIgnoreCase(InfoSchemaDb.DATABASE_NAME)) {
dbName = InfoSchemaDb.DATABASE_NAME;
} else if (dbName.equalsIgnoreCase(MysqlDb.DATABASE_NAME)) {
dbName = MysqlDb.DATABASE_NAME;
} else {
// Apply case-insensitive lookup for non-system databases
String localDbName = getLocalDatabaseName(dbName, false);
if (localDbName != null) {
dbName = localDbName;
}
}
// must use full qualified name to generate id.
// otherwise, if 2 catalogs have the same db name, the id will be the same.
return metaCache.getMetaObj(dbName, Util.genIdByName(name, dbName)).orElse(null);
}

public String bindBrokerName() {
return catalogProperty.getProperties().get(HMSExternalCatalog.BIND_BROKER_NAME);
}
// ATTN: this method only return all cached databases.
// will not visit remote datasource's metadata
@Override

When an HMS CREATE_DATABASE or ALTER_DATABASE rename event carries a mixed-case remote name while lower_case_database_names = 1, the event path may therefore publish a raw-case local database name into the database names snapshot, the database object cache, and the dbIdToName navigation map. Later normal mode-1 lookups will canonicalize the lookup name to lowercase first, so the event-published state no longer follows the same local-key convention as ordinary database lookup and reload paths.

This problem is closely related to item 5, but it is not identical:

  • item 5 is about generating an unstable database ID from the remote name instead of the canonical local name;
  • this item is about publishing the event-created database state under a non-canonical local cache key.

Both should be fixed by resolving the canonical local database name first and then using that same name consistently for event registration, cache publication, invalidation, and ID generation.

7. Database/table CREATE, DROP, and RENAME events do not invalidate connector-owned caches

Status: Open. This is present after connector SPI PR #64304 and is not introduced by #65126.

After #64304, HMS table metadata, partition metadata, column statistics, derived partition views, and file listings can be owned by the connector. The event driver currently applies database/table REGISTER, UNREGISTER, and RENAME descriptors only through FE CatalogMgr mutations. Those mutations update FE names/object/ID state and engine caches, but they do not call Connector.invalidateTable or Connector.invalidateDb.

REFRESH TABLE, REFRESH DATABASE, direct connector DDL/replay, and partition events already invalidate connector caches. Database/table incremental events are the remaining asymmetric path.

One concrete failure sequence is:

  1. Query an HMS table and warm connector-side table/schema caches.
  2. Drop the table remotely.
  3. Recreate a table with the same database/table name but a different schema.
  4. Process the DROP and CREATE events.
  5. FE state converges to the recreated object, but the connector may continue serving the old table metadata until TTL or an explicit refresh.

A same-name ALTER VIEW recreate has the same risk because it is represented as an unregister/register table descriptor.

Suggested fix for item 7

  • Invalidate the connector source key before UNREGISTER.
  • Invalidate the connector target key before REGISTER.
  • Invalidate both source and target for database/table RENAME.
  • Use remote names directly from MetastoreChangeDescriptor and do not perform remote lookup or catalog warming.
  • Add tests for same-name DROP+CREATE, same-name view recreate, and cross-database rename.

This item may be fixed in a follow-up PR because it is a #64304 master-baseline gap rather than a regression introduced by #65126. Until then, #65126 should describe its rename guarantee as FE metadata-cache convergence rather than complete connector-cache convergence.

Activity

  1. zclllyybb commented on Jul 17, 2026

    @zclllyybb
    Contributor

    Breakwater-GitHub-Analysis-Slot: slot_8fa76c90e7f5
    This content is generated by AI for reference only.

    Initial triage: I re-read the live issue and checked the referenced code at 3a75387e61388bd886eeef38b794d5ed4eb298bf. I also refreshed upstream/master to 9a5afd4958f2794a2dd458b2e7463315e9746ce3; the relevant files have no diff from the issue commit, so these observations still apply to the current master snapshot I inspected.

    Judgment:

    • The report looks valid and is best handled as four independent FE HMS incremental-event cache consistency bugs.
    • I did not find evidence in the code path that makes any of the four cases intentionally safe.

    Code-backed details:

    1. Rename cancellation path: confirmed. AlterTableEvent.processRename() calls externalTableExistInLocal(tableAfter...); for HMS, this reaches HMSExternalCatalog.tableExistInLocal(), which calls getDbNullable() and hmsExternalDatabase.getTable(tblName). ExternalCatalog.getDbNullable() and ExternalDatabase.getTableNullable() both use metaCache.getMetaObj(...), and MetaCache.getMetaObj() loads on miss. That makes the check not cache-only. If HMS already exposes the renamed target, the check can materialize the new table and return true; processRename() then returns before unregistering tableBefore, so the old local name can remain. AlterDatabaseEvent.processRename() has the analogous risk because catalog.getDbNullable(dbAfter.getName()) is also load-through; getDbForReplay(...) shows there is already a cache-only pattern available for databases.

    2. Partition events: confirmed, with a concurrency nuance. MetaCacheEntry has generation-based protection against stale in-flight manual loads, but only when an invalidation actually happens. In HiveExternalMetaCache.PartitionCacheCoordinator.addPartitionsCache() and dropPartitionsCache(), the code returns when the partition-values entry/key is not present, so no generation is bumped for an in-flight partition-values load. In dropPartitionsCache(), this early return also prevents invalidatePartitionCache(...) from running, so stale partition metadata and file-listing entries can survive after a DROP. The lower-level invalidation helper can already fall back to table-id + partition-values matching when the exact partition metadata is cold, but the current DROP event path can skip calling that helper entirely.

    3. Visibility filters: confirmed. Full names loading applies include_database_list, exclude_database_list, and include_table_list through ExternalCatalog.getFilteredDatabaseNames() and ExternalCatalog.listTableNames(). Incremental CREATE paths bypass those predicates: CreateDatabaseEvent -> CatalogMgr.registerExternalDatabaseFromEvent() -> HMSExternalCatalog.registerDatabase() updates the catalog meta cache directly, and CreateTableEvent -> CatalogMgr.registerExternalTableFromEvent() -> ExternalDatabase.registerTable() updates the DB meta cache and lowerCaseToTableName directly. If the names cache is hot, a filtered object can be inserted into the visible cached names.

    4. ExternalMetaIdMgr mapping: confirmed. DropPartitionEvent.transferToMetaIdMappings() emits META_OBJECT_TYPE_DATABASE for a partition delete. ExternalMetaIdMgr.handleDelMetaIdMapping() interprets DATABASE delete by removing the whole dbNameToMgr entry, while PARTITION delete would only remove partitionNameToMgr under the table. This is a direct type mismatch.

    Missing information / validation needed:

    • For (1), a small HMS rename regression test should assert that the target-name existence probe is cache-only and that the old local table/db name is always removed.
    • For (2), a regression/unit test should cover a DROP with a hot lower-level partition/file cache but a cold partition-values cache, plus an in-flight partition-values load where the event occurs before the load publishes.
    • For (3), tests should cover both database and table include/exclude filters with a pre-warmed names cache before processing CREATE_DATABASE / CREATE_TABLE events.
    • For (4), an event mapping replay test should verify DROP_PARTITION deletes only the partition mapping and preserves sibling table/db mappings.

    Suggested next steps:

    • Split into separate PRs if that is easier to review: rename cache-only existence, partition event fencing/invalidation, shared visibility predicates for incremental register paths, and the DROP_PARTITION meta-id type fix.
    • Prefer sharing the same database/table visibility helper between full-list loading and event registration, so incremental events cannot drift from catalog-list semantics.
    • For partition events, make DROP invalidate partition metadata/file cache entries before attempting the hot partition-values snapshot update, and make ADD/DROP invalidate or generation-bump the partition-values key when the entry exists but the value is cold/loading.
    • For rename, use cache-only lookup APIs similar to getDbForReplay / getTableForReplay for local-existence checks, and avoid returning before removing the old name.
  2. wenzhenghu commented on Jul 19, 2026

    @wenzhenghu
    ContributorAuthor

    Update: item 1 in this issue is fixed by the current FE patch set.

    What changed:

    • AlterTableEvent.processRename() no longer short-circuits on target-name existence. It now always converges local state by unregistering the old table name and registering the new table name.
    • AlterDatabaseEvent.processRename() now follows the same converge-state pattern for database rename.
    • The old tableExistInLocal() helper chain has been removed entirely because it no longer has any production callers.

    Why this closes item 1:

    • The original bug was not only the load-through local-existence check itself, but also the broader race where the target name could already be hot before the HMS rename event arrived.
    • The new logic makes rename handling idempotent and cache-state convergent, so both cases are covered: the target may be cold or already hot, but the old local name/object/id mapping and engine cache are still cleaned up.

    Validation:

    • Added FE event-level tests covering:
      • table rename with cold target
      • table rename with already-hot target
      • database rename with cold target
      • database rename with already-hot target
    • The cold-target tests also assert that the event publishes the new name/id mapping without warming the cold object cache.

    Status of the remaining items in this issue:

    • Item 2: not addressed by this patch set.
    • Item 3: still open; incremental CREATE_DATABASE/CREATE_TABLE visibility filtering still needs a separate fix.
    • Item 4: not addressed by this patch set.

    So this issue should remain open, but item 1 can be considered resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions