Search before asking
I searched existing Apache Doris issues and did not find an issue covering these HMS incremental-event cache consistency problems.
Version
Apache Doris master at 3a75387e61388bd886eeef38b794d5ed4eb298bf.
These problems were found while reviewing #65126, but they are already present on the current master baseline and were not introduced by that PR.
What's Wrong?
There are four independent correctness gaps in HMS incremental event handling.
1. ALTER TABLE/ALTER DATABASE rename can be incorrectly cancelled by a load-through "local existence" check
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.
2. ADD/DROP PARTITION events do not fence an in-flight partition-values load, and DROP may skip lower-level cache invalidation
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
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.
What You Expected?
- Rename events should inspect only locally published cache state and should always remove the old local name.
- 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.
How to Reproduce?
Rename
- Enable HMS incremental event synchronization.
- Ensure the target name is not locally cached.
- Rename
old_table to new_table in Hive.
- Process the ALTER_TABLE event.
- Observe that the existence check loads
new_table from HMS and cancels the event; cached old_table state 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=1 through HMS and process the DROP_PARTITION event.
- Recreate
p=1 with 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.
Suggested Fixes
- Introduce a cache-only local-existence check for rename handling. Always invalidate the old name; only skip publishing the new object when it is already locally present.
- 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_PARTITION and add an event mapping unit test.
The four fixes can be implemented as separate PRs if preferred.
Search before asking
I searched existing Apache Doris issues and did not find an issue covering these HMS incremental-event cache consistency problems.
Version
Apache Doris
masterat3a75387e61388bd886eeef38b794d5ed4eb298bf.These problems were found while reviewing #65126, but they are already present on the current master baseline and were not introduced by that PR.
What's Wrong?
There are four independent correctness gaps in HMS incremental event handling.
1. ALTER TABLE/ALTER DATABASE rename can be incorrectly cancelled by a load-through "local existence" check
AlterTableEvent.processRename()callsexternalTableExistInLocal()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
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
After HMS has renamed
old_tabletonew_table, queryingnew_tablefrom this check can load it from HMS and returntrue. The event is then cancelled, and cached state forold_tablemay remain.AlterDatabaseEvent.processRename()has the same structural problem throughcatalog.getDbNullable(dbAfter.getName()), although database rename is less commonly reachable in standard Hive.2. ADD/DROP PARTITION events do not fence an in-flight partition-values load, and DROP may skip lower-level cache invalidation
Both
addPartitionsCache()anddropPartitionsCache()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
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
Lines 748 to 789 in 3a75387
This causes two problems:
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, andinclude_table_list:doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 551 to 616 in 3a75387
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 343 to 356 in 3a75387
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
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java
Lines 649 to 663 in 3a75387
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
DropPartitionEvent.transferToMetaIdMappings()usesMETA_OBJECT_TYPE_DATABASEinstead ofMETA_OBJECT_TYPE_PARTITION:doris/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/event/DropPartitionEvent.java
Lines 142 to 152 in 3a75387
ExternalMetaIdMgrinterprets 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
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.
What You Expected?
How to Reproduce?
Rename
old_tabletonew_tablein Hive.new_tablefrom HMS and cancels the event; cachedold_tablestate may remain.Partition cache
p=1.p=1through HMS and process the DROP_PARTITION event.p=1with a different location/files.Visibility filter
include_table_list.External metadata ID mapping
ExternalMetaIdMgr.Suggested Fixes
META_OBJECT_TYPE_PARTITIONand add an event mapping unit test.The four fixes can be implemented as separate PRs if preferred.