Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
798dedf
[fix](fe) Invalidate external row count cache after metadata changes
924060929 Sep 24, 2026
dbc3f2b
[fix](fe) Fence external row counts across cache refreshes
924060929 Sep 28, 2026
fed6afc
[fix](fe) Scope cold external database replay invalidation
924060929 Sep 28, 2026
315e408
[fix](fe) Fence external metadata invalidation at known scopes
924060929 Sep 28, 2026
4301c12
[test](fe) Verify row-count fence during database generation swap
924060929 Sep 28, 2026
bb9889c
[fix](fe) Fence cold metadata retirement and scope Paimon SDK invalid…
924060929 Sep 29, 2026
1a99da7
[fix](fe) Fence row counts before table invalidation and preserve dro…
924060929 Sep 29, 2026
1557455
[fix](fe) Fence cache invalidation and preserve replay name semantics
924060929 Sep 29, 2026
ef38b50
[fix](fe) Preserve resolved external drop targets in replay logs
924060929 Sep 29, 2026
a9e5024
[fix](fe) Limit no-op external cache retirement and filtered events
924060929 Sep 29, 2026
0a4e111
[fix](fe) Preserve external cache identity across HMS events and ALTER
924060929 Sep 29, 2026
de4f82f
[fix](fe) Keep external refresh cleanup outside catalog lock
924060929 Sep 29, 2026
0c37f51
[fix](fe) Fence external metadata cache transitions
924060929 Sep 29, 2026
04fa4af
[fix](fe) Keep external database cache identities locale independent
924060929 Sep 29, 2026
d962085
[fix](fe) Bound external row-count database invalidation
924060929 Sep 30, 2026
43a27b0
[fix](fe) Fence cold external refresh identities
924060929 Sep 30, 2026
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 @@ -3580,7 +3580,7 @@ public void replayDropDb(DropDbInfo info) throws DdlException {
} else {
ExternalCatalog externalCatalog = (ExternalCatalog) catalogMgr.getCatalog(info.getCtlName());
if (externalCatalog != null) {
externalCatalog.replayDropDb(info.getDbName());
externalCatalog.replayDropDb(info.getDbName(), info.getResolvedDbName());
}
}
}
Expand Down
182 changes: 164 additions & 18 deletions fe/fe-core/src/main/java/org/apache/doris/catalog/RefreshManager.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,13 @@ private RemovedCatalog removeCatalog(long catalogId) {
}
String catalogName = catalog.getName();
Env.getCurrentEnv().getRefreshManager().removeFromRefreshMap(catalogId);
// Remove both lock-free mappings first: a row-count load admitted after the fence below
// could otherwise still resolve this catalog through them and alias a same-name
// replacement, because RowCountKey equality uses only tableId. Fence last, while this
// write lock still excludes a same-name CREATE.
idToCatalog.remove(catalogId);
nameToCatalog.remove(catalogName);
Env.getCurrentEnv().getExtMetaCacheMgr().invalidateRowCountCache(catalogId);
return new RemovedCatalog(catalog, catalogName);
}

Expand Down Expand Up @@ -476,7 +481,11 @@ && hasLanceIdentityKeyChange(oldProperties, newProperties)
}
log.setNewProps(loggedProperties);
replayAlterCatalogProps(log, oldProperties, false);
Env.getCurrentEnv().getEditLog().logCatalogLog(OperationType.OP_ALTER_CATALOG_PROPS, log);
if (!(catalog instanceof ExternalCatalog)) {
// External catalogs journal the committed change from the fenced helper once publication
// has happened; non-external catalogs only reach this point on success.
Env.getCurrentEnv().getEditLog().logCatalogLog(OperationType.OP_ALTER_CATALOG_PROPS, log);
}
} finally {
writeUnlock();
}
Expand Down Expand Up @@ -931,15 +940,38 @@ private void alterExternalCatalogPropsFenced(ExternalCatalog externalCatalog, Ca
Integer[] sec = {metadataRefreshIntervalSec, metadataRefreshIntervalSec};
Env.getCurrentEnv().getRefreshManager().addToRefreshMap(catalogId, sec);
}
externalCatalog.modifyCatalogProps(newProps);
// The commit reset the catalog's execution context and closed its SDK resources. Cached
// base generations and projections are bound to the replaced context; retire them now so
// the next statement loads a generation the planning fences accept, instead of retrying
// against an unplannable cached generation until managed refresh.
// against an unplannable cached generation until managed refresh. The properties are
// published before the reset's throwable cleanup, so retirement must run either way.
Env currentEnv = Env.getCurrentEnv();
ExternalMetaCacheMgr cacheMgr = currentEnv == null ? null : currentEnv.getExtMetaCacheMgr();
if (cacheMgr != null) {
cacheMgr.onCatalogOperationalContextChanged(externalCatalog.getId());
try {
if (cacheMgr != null) {
// Close the old row-count generation before publishing the new catalog context.
// The completion fence below also retires values loaded during the reset.
cacheMgr.invalidateRowCountCache(externalCatalog.getId());
}
externalCatalog.modifyCatalogProps(newProps);

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] Complete the property-specific transition when reset cleanup throws. The properties have already been published here, and the outer finally now journals them, but a JDBC closeClient() exception exits notifyPropertiesUpdated before JdbcExternalCatalog.resetToUninitialized rebuilds identifierMapping and before the generic hook removes cache groups for schema TTL, catalog weight, or meta.cache.* changes. This fallback only empties entries, so the leader can keep old name-mapping semantics and old cache policy for a durably committed ALTER (while a follower that replays cleanly uses the new values). Preserve the cleanup exception if needed, but still run the connector-derived and property-specific post-publication transitions; add a throwing-close regression that verifies the new mapping and rebuilt policy group.

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] Keep connector cleanup failures out of fatal property replay. A live ALTER now deliberately journals after properties are published even when reset/close throws, but an already-initialized follower replays that record through the same modifyCatalogProps call here. A JDBC closeClient, Trino shutdown, or similar local cleanup failure survives this finally, reaches EditLog.loadJournal's generic exception handler, and terminates the follower with System.exit(-1). That makes a committed metadata record capable of killing a live follower because of local resource cleanup. During replay, complete the required derived-state/cache transitions and log/suppress the cleanup failure; add an isReplay=true throwing-close regression.

} catch (RuntimeException e) {
if (!isReplay) {
throw e;
}
// A follower must not terminate because a local connector cleanup failed while applying
// an already-durable ALTER record. The property publication and the derived-state
// transitions above are failure-safe, so the record is considered applied.
LOG.warn("Failed to complete local cleanup while replaying ALTER CATALOG for {}: {}",
externalCatalog.getName(), e.getMessage(), e);
} finally {
if (cacheMgr != null) {
cacheMgr.onCatalogOperationalContextChanged(externalCatalog.getId());

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] Publish an opening row-count fence before this ALTER exposes the new metadata generation. modifyCatalogProps resets the catalog and swaps its DB-object cache before this new final onCatalogOperationalContextChanged fence runs. A cold DB has no removal callback, so a concurrent query can rebuild a table under the new properties and read its old-target completed count during that interval; the later fence cannot retract the plan. Keep the completion fence as well, and add a latch test between retireObjects() and this call. The earlier property-commit thread covered a missing final fence; the explicit REFRESH CATALOG opening-fence thread is a different path.

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 537ea2c. ALTER CATALOG now fences catalog row counts before modifyCatalogProps, retains the completion fence, and tests the call order across property publication.

}
if (!isReplay && currentEnv != null) {

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] Validate JDBC name mapping before journaling this ALTER. A JDBC SET PROPERTIES ("meta_names_mapping" = "not-json") passes checkProperties, so modifyCatalogProps publishes it before JdbcIdentifierMapping throws during reset. This finally then journals the failed ALTER; replay suppresses the mapping exception, leaving durable invalid properties and the old derived mapping on leader and followers. The journal call was skipped for this failure before this change. Validate the effective mapping before publication and add a malformed-mapping ALTER/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 8b8afb2. JDBC mapping candidates are parsed during pre-publication ALTER validation. A malformed value now raises DdlException before property mutation or edit-log publication. Added a CatalogMgr test proving both properties and journal stay unchanged.

// Publication has happened once modifyCatalogProps is reached; journal it even if the
// reset's own cleanup throws. Pre-publication validation failures never reach here.
currentEnv.getEditLog().logCatalogLog(OperationType.OP_ALTER_CATALOG_PROPS, log);
}
}
}

Expand Down Expand Up @@ -1039,7 +1071,11 @@ public void unregisterExternalDatabase(String dbName, String catalogName)
if (!(catalog instanceof ExternalCatalog)) {
throw new DdlException("Only support drop ExternalCatalog databases");
}
((HMSExternalCatalog) catalog).unregisterDatabase(dbName);
HMSExternalCatalog hmsCatalog = (HMSExternalCatalog) catalog;
if (hmsCatalog.isDatabaseEventTargetExcluded(dbName)) {
return;
}
hmsCatalog.unregisterDatabase(dbName);
}

public void registerExternalDatabaseFromEvent(String dbName, String catalogName)
Expand All @@ -1054,7 +1090,7 @@ public void registerExternalDatabaseFromEvent(String dbName, String catalogName)

HMSExternalCatalog hmsCatalog = (HMSExternalCatalog) catalog;
long metadataLoadEpoch = hmsCatalog.acquireMetadataLoadEpoch();
long dbId = Util.genIdByName(catalogName, dbName);
long dbId = Util.genIdByName(catalogName, hmsCatalog.localDatabaseNameFromRemote(dbName));
// -1L means it will be dropped later, ignore
if (dbId == ExternalMetaIdMgr.META_ID_FOR_NOT_EXISTS) {
return;
Expand All @@ -1075,8 +1111,29 @@ public void addExternalPartitions(String catalogName, String dbName, String tabl
if (!(catalog instanceof ExternalCatalog)) {
throw new DdlException("Only support ExternalCatalog");
}
if (catalog instanceof HMSExternalCatalog
&& ((HMSExternalCatalog) catalog).isPartitionEventTargetExcluded(dbName, tableName)) {
return;
}
// Partition events are already committed remotely. Fence the row count by cached identity
// before any database/table reload can fail and make the ignored-not-found path return.
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateRowCountCache(catalog.getId(), dbName, tableName);
try {
addExternalPartitionsAfterFence(catalog, dbName, tableName, partitionNames, updateTime,
ignoreIfNotExists);
} finally {
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateRowCountCache(catalog.getId(), dbName, tableName);
}
}

private void addExternalPartitionsAfterFence(CatalogIf catalog, String dbName, String tableName,
List<String> partitionNames, long updateTime, boolean ignoreIfNotExists) throws DdlException {
DatabaseIf db = catalog.getDbNullable(dbName);

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] Skip invalidation for HMS events excluded by catalog filters. In a mode-2 HMS catalog that excludes archived while sales is hot, an ADD PARTITION event for archived.t still reaches this ignored-not-found branch: the event stream is unfiltered, getDbNullable cannot find the excluded DB, and invalidateTableByNameOrWider has no canonical mapping, so it calls invalidateCatalog and clears sales engine and row-count caches. Excluded tables in a visible DB similarly flush their sibling tables at DB scope. The merge-base branches returned without invalidation; distinguish filtered targets from a lost identity before widening the fence, and cover both filters with hot unrelated entries.

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 182ef43. HMS now checks the catalog database and include-table filters before any row-count or engine-cache invalidation. All three notification routes are covered: add/drop partition in CatalogMgr and alter-partition refresh in RefreshManager. CatalogMgrTest exercises excluded ARCHIVED.t and sales.cold alongside an included sales.hot target and verifies no cache-manager interaction for the excluded events. The 176 focused FE tests and full FE build pass.

if (db == null) {
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateTableByNameOrWider(catalog.getId(), dbName, tableName);
if (!ignoreIfNotExists) {
throw new DdlException("Database " + dbName + " does not exist in catalog " + catalog.getName());
}
Expand All @@ -1085,6 +1142,8 @@ public void addExternalPartitions(String catalogName, String dbName, String tabl

TableIf table = db.getTableNullable(tableName);
if (table == null) {
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateTableByNameOrWider(catalog.getId(), dbName, tableName);
if (!ignoreIfNotExists) {
throw new DdlException("Table " + tableName + " does not exist in db " + dbName);
}
Expand Down Expand Up @@ -1118,8 +1177,29 @@ public void dropExternalPartitions(String catalogName, String dbName, String tab
if (!(catalog instanceof ExternalCatalog)) {
throw new DdlException("Only support ExternalCatalog");
}
if (catalog instanceof HMSExternalCatalog
&& ((HMSExternalCatalog) catalog).isPartitionEventTargetExcluded(dbName, tableName)) {
return;
}
// Partition events are already committed remotely. Fence the row count by cached identity
// before any database/table reload can fail and make the ignored-not-found path return.
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateRowCountCache(catalog.getId(), dbName, tableName);
try {
dropExternalPartitionsAfterFence(catalog, dbName, tableName, partitionNames, updateTime,
ignoreIfNotExists);
} finally {
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateRowCountCache(catalog.getId(), dbName, tableName);
}
}

private void dropExternalPartitionsAfterFence(CatalogIf catalog, String dbName, String tableName,
List<String> partitionNames, long updateTime, boolean ignoreIfNotExists) throws DdlException {
DatabaseIf db = catalog.getDbNullable(dbName);
if (db == null) {
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateTableByNameOrWider(catalog.getId(), dbName, tableName);
if (!ignoreIfNotExists) {
throw new DdlException("Database " + dbName + " does not exist in catalog " + catalog.getName());
}
Expand All @@ -1128,6 +1208,8 @@ public void dropExternalPartitions(String catalogName, String dbName, String tab

TableIf table = db.getTableNullable(tableName);
if (table == null) {
Env.getCurrentEnv().getExtMetaCacheMgr()
.invalidateTableByNameOrWider(catalog.getId(), dbName, tableName);
if (!ignoreIfNotExists) {
throw new DdlException("Table " + tableName + " does not exist in db " + dbName);
}
Expand Down
Loading
Loading