Repository navigation
Description
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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/event/AlterTableEvent.java
Lines 107 to 123 in 3a75387
| 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalCatalog.java
Lines 190 to 198 in 3a75387
| @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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
Lines 707 to 746 in 3a75387
| 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); | |
| } | |
| } |
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
Lines 748 to 789 in 3a75387
| 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 551 to 616 in 3a75387
| 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)); | |
| } |
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 343 to 356 in 3a75387
| * @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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalCatalog.java
Lines 205 to 216 in 3a75387
| @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())); | |
| } |
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java
Lines 649 to 663 in 3a75387
| // 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/event/DropPartitionEvent.java
Lines 142 to 152 in 3a75387
| @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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalMetaIdMgr.java
Lines 145 to 169 in 3a75387
| 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
Lines 772 to 789 in 54e0e97
| 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 459 to 466 in 54e0e97
| 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)
- Enable HMS incremental event synchronization.
- Ensure the target name is not locally cached.
- Rename
old_tabletonew_tablein Hive. - Process the ALTER_TABLE event.
- On the affected baseline, the existence check loads
new_tablefrom HMS and cancels the event; cachedold_tablestate may remain.
Partition cache
- Cache partition metadata/file listings for
p=1. - Evict or invalidate only the table's partition-values entry, or block an in-flight partition-values load.
- Drop
p=1through HMS and process the DROP_PARTITION event. - Recreate
p=1with a different location/files. - The old partition metadata or file listing may still be reused.
Visibility filter
- Create an HMS catalog with an include/exclude database filter or
include_table_list. - Warm the corresponding names cache.
- Create a filtered-out database/table directly in Hive.
- Process the HMS CREATE event.
- Observe that the filtered object is inserted into the cached names and becomes visible/queryable.
External metadata ID mapping
- Add a database/table/partition mapping to
ExternalMetaIdMgr. - Process the partition's DROP_PARTITION mapping.
- Observe that the database mapping subtree is removed instead of only the partition mapping.
Database ID stability
- Configure an HMS catalog with
lower_case_database_names = 1. - Process an ALTER_DATABASE rename event whose new remote name contains uppercase characters.
- Observe that
registerExternalDatabaseFromEvent()generates the new ID from the remote mixed-case name while the local cache key is lowercase. - Reset or reload the catalog so the normal database-object loader runs.
- Observe that the reloaded database ID is generated from the lowercase local name and differs from the event-created ID.
Database local-key publication
- Configure an HMS catalog with
lower_case_database_names = 1. - Process a CREATE_DATABASE or ALTER_DATABASE rename event whose remote name contains uppercase characters.
- 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. - Perform a normal database lookup that first canonicalizes the name to lowercase.
- Observe that the event-published database state no longer follows the same local-key convention as ordinary lookup and reload paths.
Suggested Fixes
- 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.
- 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.
- Reuse a shared database/table visibility predicate in both full names loading and incremental registration.
- Change the DROP_PARTITION mapping type to
META_OBJECT_TYPE_PARTITIONand add an event mapping unit test. - 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. - Reuse that same canonical local database name when publishing the event-created database into the names snapshot, object cache, and
dbIdToNamemap. 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, ...):
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalCatalog.java
Lines 196 to 205 in 54e0e97
| 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 923 to 928 in 54e0e97
| // 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:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 760 to 778 in 54e0e97
| // 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); | |
| } | |
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 1296 to 1303 in 54e0e97
| 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:
- Query an HMS table and warm connector-side table/schema caches.
- Drop the table remotely.
- Recreate a table with the same database/table name but a different schema.
- Process the DROP and CREATE events.
- 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
zclllyybb commentedon Jul 17, 2026
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 refreshedupstream/masterto9a5afd4958f2794a2dd458b2e7463315e9746ce3; the relevant files have no diff from the issue commit, so these observations still apply to the current master snapshot I inspected.Judgment:
Code-backed details:
Rename cancellation path: confirmed.
AlterTableEvent.processRename()callsexternalTableExistInLocal(tableAfter...); for HMS, this reachesHMSExternalCatalog.tableExistInLocal(), which callsgetDbNullable()andhmsExternalDatabase.getTable(tblName).ExternalCatalog.getDbNullable()andExternalDatabase.getTableNullable()both usemetaCache.getMetaObj(...), andMetaCache.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 unregisteringtableBefore, so the old local name can remain.AlterDatabaseEvent.processRename()has the analogous risk becausecatalog.getDbNullable(dbAfter.getName())is also load-through;getDbForReplay(...)shows there is already a cache-only pattern available for databases.Partition events: confirmed, with a concurrency nuance.
MetaCacheEntryhas generation-based protection against stale in-flight manual loads, but only when an invalidation actually happens. InHiveExternalMetaCache.PartitionCacheCoordinator.addPartitionsCache()anddropPartitionsCache(), the code returns when the partition-values entry/key is not present, so no generation is bumped for an in-flight partition-values load. IndropPartitionsCache(), this early return also preventsinvalidatePartitionCache(...)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.Visibility filters: confirmed. Full names loading applies
include_database_list,exclude_database_list, andinclude_table_listthroughExternalCatalog.getFilteredDatabaseNames()andExternalCatalog.listTableNames(). Incremental CREATE paths bypass those predicates:CreateDatabaseEvent->CatalogMgr.registerExternalDatabaseFromEvent()->HMSExternalCatalog.registerDatabase()updates the catalog meta cache directly, andCreateTableEvent->CatalogMgr.registerExternalTableFromEvent()->ExternalDatabase.registerTable()updates the DB meta cache andlowerCaseToTableNamedirectly. If the names cache is hot, a filtered object can be inserted into the visible cached names.ExternalMetaIdMgr mapping: confirmed.
DropPartitionEvent.transferToMetaIdMappings()emitsMETA_OBJECT_TYPE_DATABASEfor a partition delete.ExternalMetaIdMgr.handleDelMetaIdMapping()interprets DATABASE delete by removing the wholedbNameToMgrentry, while PARTITION delete would only removepartitionNameToMgrunder the table. This is a direct type mismatch.Missing information / validation needed:
Suggested next steps:
getDbForReplay/getTableForReplayfor local-existence checks, and avoid returning before removing the old name.wenzhenghu commentedon Jul 19, 2026
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.tableExistInLocal()helper chain has been removed entirely because it no longer has any production callers.Why this closes item 1:
Validation:
Status of the remaining items in this issue:
So this issue should remain open, but item 1 can be considered resolved.
[fix](fe) Fix HMS rename cache convergence
[fix](fe) Fix HMS rename cache convergence