924060929 commented on code in PR #69720:
URL: https://github.com/apache/doris/pull/69720#discussion_r4396985531


##########
fe/fe-core/src/main/java/org/apache/doris/catalog/RefreshManager.java:
##########
@@ -162,17 +162,14 @@ public void handleRefreshTable(String catalogName, String 
dbName, String tableNa
             return;
         }
 
-        TableIf table = db.getTableNullable(tableName);
-        if (table == null) {
-            if (!ignoreIfNotExists) {
-                throw new DdlException("Table " + tableName + " does not exist 
in db " + dbName);
-            }
-            return;
-        }
-        long updateTime = System.currentTimeMillis();
-        refreshTableInternal((ExternalDatabase) db, (ExternalTable) table, 
updateTime, true);
+        // A forwarding FE may retain a dropped table that is cold on the 
master. Refresh by name so
+        // remote absence cannot prevent invalidating that FE's stale lookup 
through the edit log.
+        ExternalDatabase<?> externalDb = (ExternalDatabase<?>) db;
+        String localTableName = externalDb.getTableForReplay(tableName)
+                .map(ExternalTable::getName).orElse(tableName);
         ExternalObjectLog log = 
ExternalObjectLog.createForExplicitRefreshTable(
-                catalog.getId(), db.getFullName(), table.getName(), 
updateTime);
+                catalog.getId(), db.getFullName(), localTableName, 
System.currentTimeMillis());
+        replayRefreshTable(log);

Review Comment:
   Could we make the new invalidation contract explicit and share the 
implementation through a private helper, instead of calling the replay entry 
point from command execution?
   
   The name-based fix is appropriate for the reported failure. A follower can 
retain a dropped table while the master's table cache is cold. 
`getTableNullable()` is a loading cache lookup, not an authoritative remote 
existence check: a hot master can return the stale object, whereas a cold 
master loads names and can return `null`. 
`ExternalDatabase.buildTableForInit()` also converts some remote-access 
failures into `null`. Making invalidation depend on that lookup therefore gives 
the same refresh request different outcomes depending on cache state.
   
   Changing `ignoreIfNotExists` to `true` alone would still return before 
invalidation/journaling. Keeping the lookup and removing the early return would 
handle a missing-table result, but retain an unnecessary synchronous 
table-metadata dependency. It would also make a missing database silently 
succeed. At this head, the only production caller and all six test calls pass 
`false`, so there is no active two-mode API to preserve.
   
   I suggest:
   - Remove `ignoreIfNotExists` from this method and its callers; keep 
missing-catalog/type/database validation and the existing command privilege 
check.
   - Keep the cache-only table-name resolution added by this PR. A missing 
table object must not prevent recording an invalidation for other FEs.
   - Extract the ordinary refresh application from `replayRefreshTable()` into 
a private helper. Command execution owns validation and journaling; replay owns 
journal checks and rename dispatch; both apply exactly the same cache effects.
   - Keep the journal's **`invalidateTableLookup`** flag: it is different from 
`ignoreIfNotExists` and is still needed to distinguish explicit lookup 
invalidation from content-only refresh.
   
   The explicit contract would be: **refreshing a table name in an existing 
external database invalidates metadata even if the remote table no longer 
exists; success does not assert remote existence or preload its schema.** This 
intentionally also permits a never-existing/misspelled table name, so please 
document that user-visible behavior in the REFRESH documentation as well as the 
release note.
   
   Complete proposed replacements for `handleRefreshTable()` and 
`replayRefreshTable()`, plus the new helper:
   
   ```java
   /**
    * Invalidates metadata for a table name in an existing external database.
    * The remote table does not have to exist; a cold master may need to 
invalidate
    * stale lookups retained by other FEs.
    */
   public void handleRefreshTable(String catalogName, String dbName, String 
tableName)
           throws DdlException {
       Env env = Env.getCurrentEnv();
       CatalogIf catalog = catalogName != null ? 
env.getCatalogMgr().getCatalog(catalogName) : env.getCurrentCatalog();
       if (catalog == null) {
           throw new DdlException("Catalog " + catalogName + " doesn't exist.");
       }
       if (!(catalog instanceof ExternalCatalog)) {
           throw new DdlException("Only support refresh ExternalCatalog 
Tables");
       }
   
       ExternalCatalog externalCatalog = (ExternalCatalog) catalog;
       ExternalDatabase<?> db = externalCatalog.getDbNullable(dbName);
       if (db == null) {
           throw new DdlException("Database " + dbName + " does not exist in 
catalog " + catalog.getName());
       }
   
       // Retain the canonical local identity when cached, without loading a 
remote table.
       String localTableName = db.getTableForReplay(tableName)
               .map(ExternalTable::getName)
               .orElse(tableName);
       long updateTime = System.currentTimeMillis();
       ExternalObjectLog log = ExternalObjectLog.createForExplicitRefreshTable(
               catalog.getId(), db.getFullName(), localTableName, updateTime);
   
       applyTableRefresh(externalCatalog, db.getFullName(), localTableName, 
updateTime, true);
       env.getEditLog().logRefreshExternalTable(log);
   }
   
   public void replayRefreshTable(ExternalObjectLog log) {
       ExternalCatalog catalog = (ExternalCatalog) 
Env.getCurrentEnv().getCatalogMgr().getCatalog(log.getCatalogId());
       if (catalog == null) {
           LOG.warn("failed to find catalog when replaying refresh table: {}", 
log.debugForRefreshTable());
           return;
       }
   
       String localDbName = log.getDbName();
       String localTableName = log.getTableName();
       if (Strings.isNullOrEmpty(localDbName) || 
Strings.isNullOrEmpty(localTableName)) {
           LOG.warn("refresh table replay log has no local name: {}", 
log.debugForRefreshTable());
           return;
       }
       if (!Strings.isNullOrEmpty(log.getNewTableName())) {
           replayRenameTable(log, catalog, catalog.getDbForReplay(localDbName));
           return;
       }
   
       applyTableRefresh(catalog, localDbName, localTableName,
               log.getLastUpdateTime(), log.isInvalidateTableLookup());
   }
   
   /**
    * Applies refresh effects without synchronously loading remote table 
metadata.
    * Shared by command execution and journal replay; does not write an edit 
log.
    */
   private void applyTableRefresh(ExternalCatalog catalog, String localDbName, 
String localTableName,
           long updateTime, boolean invalidateTableLookup) {
       long catalogId = catalog.getId();
       long dbId = Util.genIdByName(catalog.getName(), localDbName);
       long tableId = Util.genIdByName(catalog.getName(), localDbName, 
localTableName);
       boolean invalidateDatabase = invalidateTableLookup && 
catalog.getLowerCaseTableNames() != 0;
       ExternalMetaCacheMgr cacheMgr = Env.getCurrentEnv().getExtMetaCacheMgr();
   
       Optional<ExternalDatabase<? extends ExternalTable>> db = 
catalog.getDbForReplay(localDbName);
       if (!db.isPresent()) {
           try {
               // Remote name mappings are unavailable without a retained 
database object.
               invalidateAllConnectorCachesIfPresent(catalog);
           } finally {
               if (invalidateDatabase) {
                   cacheMgr.invalidateDb(catalogId, dbId, localDbName);
               } else {
                   cacheMgr.invalidateTable(catalogId, dbId, localDbName, 
tableId, localTableName);
               }
           }
           LOG.info("database object cache is cold when refreshing table; "
                           + "invalidated caches by local names {}.{} in 
catalog {}",
                   localDbName, localTableName, catalogId);
           return;
       }
   
       ExternalDatabase<? extends ExternalTable> database = db.get();
       Optional<? extends ExternalTable> table = 
database.getTableForReplay(localTableName);
       if (table.isPresent()) {
           // Preserve generation fencing, object-created state and metadata 
update time.
           refreshTableInternal(database, table.get(), updateTime, 
invalidateTableLookup);
           return;
       }
   
       try {
           if (catalog instanceof PluginDrivenExternalCatalog) {
               // A cold table object cannot provide its mapped remote name.
               ((PluginDrivenExternalCatalog) 
catalog).getConnector().invalidateDb(database.getRemoteName());
           }
       } finally {
           try {
               if (invalidateTableLookup) {
                   database.invalidateTableLookup(localTableName);
               }
           } finally {
               // Case-insensitive lookup may refer to a different retained 
spelling/table ID.
               if (invalidateDatabase) {
                   cacheMgr.invalidateDb(catalogId, dbId, 
database.getFullName());
               } else {
                   cacheMgr.invalidateTable(
                           catalogId, dbId, database.getFullName(), tableId, 
localTableName);
               }
           }
       }
       LOG.info("table object cache is cold when refreshing table; "
                       + "invalidated caches by local name {}.{} in catalog {}",
               database.getFullName(), localTableName, catalogId);
   }
   ```
   
   Keep the existing `refreshTableAfterExternalMutation()`, 
`refreshTableInternal()`, `replayRenameTable()`, and 
`invalidateAllConnectorCachesIfPresent()` implementations. No new imports are 
required. The helper above retains the existing hot/cold paths, 
connector-before-engine/row-count invalidation order, case-insensitive 
database-wide fallback, and nested `finally` cleanup. Hot objects still use 
`refreshTableInternal()` for generation fencing and update time. The explicit 
command still applies locally before writing its existing journal; this 
proposal does not change journal format or ordering.
   
   Update the production call in `RefreshTableCommand.run()` to:
   
   ```java
   Env.getCurrentEnv().getRefreshManager()
           .handleRefreshTable(tableNameInfo.getCtl(), tableNameInfo.getDb(), 
tableNameInfo.getTbl());
   ```
   
   Also remove the trailing `, false` from all six direct test calls:
   - `RefreshManagerTest`: `testExplicitRefreshReloadsTableNamesAndObject` and 
`testExplicitRefreshOnColdMasterInvalidatesDroppedTableOnFollower`.
   - `RefreshTableTest`: `testRefreshTable`.
   - `PluginDrivenMissingTableTest`: `verifyDelayedMissing`, 
`numericLookupCapturedBeforeRefreshRepublishesItsIdentity`, and 
`foldedNamesRefreshClearsHistoricalRemoteSpellings`.
   
   The existing cold-master/warm-follower, repeat-refresh and recreation 
coverage should remain. I would additionally cover a missing database (still an 
error, with no refresh journal), a resolvable database with a cold table cache 
whose remote table-name loader fails if called (refresh must not invoke it), 
and actual mixed-case names across master/follower in modes 1/2.
   
   This is a non-blocking interface/structure recommendation, not a 
demonstrated wrong-result or crash finding. The proposed replacement code has 
been checked against the current source at `e77bfeab48b5`, but has **not** been 
compiled or tested; the PR's existing validation results do not validate this 
proposed refactor.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to