github-actions[bot] commented on code in PR #66307:
URL: https://github.com/apache/doris/pull/66307#discussion_r3843898029


##########
fe/fe-core/src/main/java/org/apache/doris/catalog/CatalogRecycleBin.java:
##########
@@ -233,6 +234,19 @@ public boolean recycleTable(long dbId, Table table, 
boolean isReplay,
         }
     }
 
+    @SuppressWarnings("deprecation")
+    public boolean containsDistributionMappingConstraint() {
+        readLock();
+        try {
+            return idToTable.values().stream()

Review Comment:
   [P1] Fence the live-to-recycle mapping transition
   
   Scanning the two stable endpoints still leaves a gap during non-swap, 
non-force `REPLACE TABLE`. That path removes the original table's global 
constraints first and only later inserts the still-mapped table object into 
`idToTable`, without holding the frontend-admission fence. An `ADD FRONTEND` in 
between sees neither a live nor recycled mapping, admits an older FE, and the 
replace then publishes the unsupported subtype into recycle/image state. Please 
hold the admission fence across that whole transition (and test the race), 
rather than relying only on endpoint scans.



##########
fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java:
##########
@@ -1541,6 +1556,41 @@ public static void loadJournal(Env env, Long logId, 
JournalEntity journal) {
         }
     }
 
+    private static void replayConstraint(
+            Env env, TableNameInfo tableNameInfo, Constraint constraint, 
boolean add) {
+        TableIf table = null;
+        if (constraint instanceof DistributionMappingConstraint) {
+            CatalogIf<?> catalog = 
env.getCatalogMgr().getCatalog(tableNameInfo.getCtl());
+            DatabaseIf<?> database = catalog == null ? null : 
catalog.getDbNullable(tableNameInfo.getDb());
+            table = database == null ? null : 
database.getTableNullable(tableNameInfo.getTbl());
+            if (table != null && !table.tryWriteLock(

Review Comment:
   [P1] Do not turn replay lock contention into a fatal error
   
   A serving follower can hold this table's read lock while planning a query. 
If that ordinary planning work lasts beyond `catalog_try_lock_timeout_ms`, this 
throws into the top-level replay handler, which calls `System.exit(-1)`, so 
committing a mapping DDL can kill an otherwise healthy follower. Replay needs 
to block or retry until the lock is available (with a replay-specific policy), 
not inherit the bounded user-DDL timeout. Please add a latch test that releases 
a long-held read lock and verifies replay subsequently succeeds.



##########
fe/fe-core/src/main/java/org/apache/doris/backup/RestoreJob.java:
##########
@@ -2586,154 +2651,184 @@ protected void cleanMetaObjects(boolean isReplay) {
     }
 
     private Status atomicReplaceOlapTables(Database db, boolean isReplay) {
+        Preconditions.checkState(db.isWriteLockHeldByCurrentThread(),
+                "atomic replacement must hold the database write lock");
+        Status validationStatus = prevalidateAtomicRestoreTargets(db);
+        if (!validationStatus.ok()) {
+            return validationStatus;
+        }
+        try {
+            
Env.getCurrentEnv().getConstraintManager().checkAndDropTableConstraints(

Review Comment:
   [P1] Invalidate dependent MTMV plans with the constraint batch drop
   
   This removes the targets' PK/UK/FK metadata, but the replacement loop only 
fences the SQL-result cache. A dependent MTMV remains keyed to the same 
qualified base-table name, so its existing `MTMVCache` can still be returned 
even though the uniqueness/FK proofs used to build that plan were just deleted. 
Explicit `DROP CONSTRAINT` now fences these generations inside the metadata 
transition; atomic restore bypasses that path. Please collect and invalidate 
all dependent MTMV caches before publishing the replacements, with equivalent 
replay coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/backup/RestoreJob.java:
##########
@@ -1200,6 +1205,8 @@ protected void allReplicasCreated() {
                             + " already exist in db: " + db.getFullName());
                     return;
                 }
+                
Env.getCurrentEnv().getConstraintManager().restoreTableConstraints(

Review Comment:
   [P1] Fence mapping-bearing restores against older frontends
   
   This restores the mapping stored in the table object without the 
frontend-version/admission protocol used by ordinary mapping ADD (the replay 
and atomic-restore calls have the same gap). For example, after taking a backup 
with a mapping, dropping the live mapping allows an older FE to be added; 
restoring that backup then serializes `backupMeta`/`restoredTbls` with the 
unknown `DistributionMappingConstraint` subtype and can break that follower's 
replay/image loading. The existing ADD/recycle checks do not cover this import 
path. Please validate mapping-bearing restores under the same admission fence 
before any such job/table is journaled or published, and cover mixed-version 
restore plus concurrent ADD FRONTEND.



##########
fe/fe-core/src/main/java/org/apache/doris/backup/RestoreJob.java:
##########
@@ -2586,154 +2651,184 @@ protected void cleanMetaObjects(boolean isReplay) {
     }
 
     private Status atomicReplaceOlapTables(Database db, boolean isReplay) {
+        Preconditions.checkState(db.isWriteLockHeldByCurrentThread(),
+                "atomic replacement must hold the database write lock");
+        Status validationStatus = prevalidateAtomicRestoreTargets(db);
+        if (!validationStatus.ok()) {
+            return validationStatus;
+        }
+        try {
+            
Env.getCurrentEnv().getConstraintManager().checkAndDropTableConstraints(
+                    getAtomicRestoreConstraintDropTargets(db), 
!isForceReplace);
+        } catch (DdlException e) {
+            return new Status(ErrCode.COMMON_ERROR,
+                    "replace table failed, reason=" + e.getMessage());
+        }
         for (String tableName : jobInfo.backupOlapTableObjects.keySet()) {
-            String originName = jobInfo.getAliasByOriginNameIfSet(tableName);
-            if (GlobalVariable.isStoredTableNamesLowerCase()) {
-                originName = originName.toLowerCase();
-            }
+            String originName = restoreTargetName(tableName);
             String aliasName = tableAliasWithAtomicRestore(originName);
 
-            if (!db.writeLockIfExist()) {
-                return Status.OK;
-            }
+            Table newTbl = db.getTableNullable(aliasName);
+            Preconditions.checkNotNull(newTbl);
+            Preconditions.checkState(newTbl.getType() == TableType.OLAP);
+            Table originTbl = db.getTableNullable(originName);
+            Preconditions.checkState(originTbl == null
+                    || originTbl.getType() == TableType.OLAP);
+            OlapTable originOlapTbl = (OlapTable) originTbl;
+
+            // replace the table.
+            OlapTable newOlapTbl = (OlapTable) newTbl;
+            newOlapTbl.writeLock();
             try {
-                Table newTbl = db.getTableNullable(aliasName);
-                if (newTbl == null) {
-                    LOG.warn("replace table from {} to {}, but the temp table 
is not found" + " isAtomicRestore: {}",
-                            aliasName, originName, isAtomicRestore);
-                    return new Status(ErrCode.COMMON_ERROR, "replace table 
failed, the temp table "
-                            + aliasName + " is not found");
-                }
-                if (newTbl.getType() != TableType.OLAP) {
-                    LOG.warn(
-                            "replace table from {} to {}, but the temp table 
is not OLAP, it type is {}"
-                                    + " isAtomicRestore: {}",
-                            aliasName, originName, newTbl.getType(), 
isAtomicRestore);
-                    return new Status(ErrCode.COMMON_ERROR, "replace table 
failed, the temp table " + aliasName
-                            + " is not OLAP table, it is " + newTbl.getType());
-                }
-
-                OlapTable originOlapTbl = null;
-                Table originTbl = db.getTableNullable(originName);
-                if (originTbl != null) {
-                    if (originTbl.getType() != TableType.OLAP) {
-                        LOG.warn(
-                                "replace table from {} to {}, but the origin 
table is not OLAP, it type is {}"
-                                        + " isAtomicRestore: {}",
-                                aliasName, originName, originTbl.getType(), 
isAtomicRestore);
-                        return new Status(ErrCode.COMMON_ERROR, "replace table 
failed, the origin table "
-                                + originName + " is not OLAP table, it is " + 
originTbl.getType());
-                    }
-                    originOlapTbl = (OlapTable) originTbl; // save the origin 
olap table, then drop it.
-                }
+                TableNameInfo originTableInfo = new TableNameInfo(
+                        InternalCatalog.INTERNAL_CATALOG_NAME, 
db.getFullName(), originName);
+                // rename new table name to origin table name and add the new 
table to database.
+                db.unregisterTable(aliasName);
+                newOlapTbl.setName(originName);
+                db.unregisterTable(originName);
+                db.registerTable(newOlapTbl);
+                
Env.getCurrentEnv().getConstraintManager().restoreTableConstraints(
+                        originTableInfo, newOlapTbl);
+                Env.getCurrentEnv().getSqlCacheManager()
+                        
.invalidateAboutTableAndFencePublication(originTableInfo);
+
+                // set the olap table state to normal immediately for querying
+                newOlapTbl.setState(OlapTableState.NORMAL);
+                LOG.info(
+                        "restore with replace table {} name to {}, and set 
state to normal, origin table={}"
+                                + " isAtomicRestore: {}",
+                        newOlapTbl.getId(), originName,
+                        originOlapTbl == null ? -1L : originOlapTbl.getId(),
+                        isAtomicRestore);
+            } finally {
+                newOlapTbl.writeUnlock();
+            }
 
-                // replace the table.
-                OlapTable newOlapTbl = (OlapTable) newTbl;
-                newOlapTbl.writeLock();
+            if (originOlapTbl != null) {
+                // The origin table is not used anymore, need to drop all its 
tablets.
+                originOlapTbl.writeLock();
                 try {
-                    // rename new table name to origin table name and add the 
new table to database.
-                    db.unregisterTable(aliasName);
-                    newOlapTbl.checkAndSetName(originName, false);
-                    db.unregisterTable(originName);
-                    db.registerTable(newOlapTbl);
-
-                    // set the olap table state to normal immediately for 
querying
-                    newOlapTbl.setState(OlapTableState.NORMAL);
-                    LOG.info(
-                            "restore with replace table {} name to {}, and set 
state to normal, origin table={}"
-                                    + " isAtomicRestore: {}",
-                            newOlapTbl.getId(), originName, originOlapTbl == 
null ? -1L : originOlapTbl.getId(),
-                            isAtomicRestore);
-                } catch (DdlException e) {
-                    LOG.warn("restore with replace table {} name from {} to 
{}, isAtomicRestore: {}",
-                            newOlapTbl.getId(), aliasName, originName, 
isAtomicRestore, e);
-                    return new Status(ErrCode.COMMON_ERROR, "replace table 
from " + aliasName + " to " + originName
-                            + " failed, reason=" + e.getMessage());
+                    LOG.info("drop the origin olap table {}. table={}" + " 
isAtomicRestore: {}",
+                            originOlapTbl.getName(), originOlapTbl.getId(), 
isAtomicRestore);
+                    Env.getCurrentEnv().onEraseOlapTable(db.getId(), 
originOlapTbl, isReplay);
                 } finally {
-                    newOlapTbl.writeUnlock();
+                    originOlapTbl.writeUnlock();
                 }
-
-                if (originOlapTbl != null) {
-                    // The origin table is not used anymore, need to drop all 
its tablets.
-                    originOlapTbl.writeLock();
-                    try {
-                        LOG.info("drop the origin olap table {}. table={}" + " 
isAtomicRestore: {}",
-                                originOlapTbl.getName(), 
originOlapTbl.getId(), isAtomicRestore);
-                        Env.getCurrentEnv().onEraseOlapTable(db.getId(), 
originOlapTbl, isReplay);
-                    } finally {
-                        originOlapTbl.writeUnlock();
-                    }
-                }
-            } finally {
-                db.writeUnlock();
             }
         }
         for (BackupJobInfo.BackupViewInfo backupViewInfo : 
jobInfo.newBackupObjects.views) {
-            String originName = 
jobInfo.getAliasByOriginNameIfSet(backupViewInfo.name);
-            if (GlobalVariable.isStoredTableNamesLowerCase()) {
-                originName = originName.toLowerCase();
-            }
+            String originName = restoreTargetName(backupViewInfo.name);
             String aliasName = tableAliasWithAtomicRestore(originName);
 
-            if (!db.writeLockIfExist()) {
-                return Status.OK;
-            }
+            Table newTbl = db.getTableNullable(aliasName);
+            Preconditions.checkNotNull(newTbl);
+            Preconditions.checkState(newTbl.getType() == TableType.VIEW);
+            Table originTbl = db.getTableNullable(originName);
+            Preconditions.checkState(originTbl == null
+                    || originTbl.getType() == TableType.VIEW);
+            View originViewTbl = (View) originTbl;
+
+            // replace the view.
+            View newViewTbl = (View) newTbl;
+            newViewTbl.writeLock();
             try {
-                Table newTbl = db.getTableNullable(aliasName);
-                if (newTbl == null) {
-                    LOG.warn("replace view from {} to {}, but the temp view is 
not found" + " isAtomicRestore: {}",
-                            aliasName, originName, isAtomicRestore);
-                    return new Status(ErrCode.COMMON_ERROR, "replace view 
failed, the temp view "
-                            + aliasName + " is not found");
-                }
-                if (newTbl.getType() != TableType.VIEW) {
-                    LOG.warn(
-                            "replace view from {} to {}, but the temp view is 
not VIEW, it type is {}"
-                                    + " isAtomicRestore: {}",
-                            aliasName, originName, newTbl.getType(), 
isAtomicRestore);
-                    return new Status(ErrCode.COMMON_ERROR, "replace view 
failed, the temp view " + aliasName
-                            + " is not OLAP, it is " + newTbl.getType());
-                }
+                // rename new view name to origin view name and add the new 
view to database.
+                db.unregisterTable(aliasName);
+                db.unregisterTable(originName);
+                newViewTbl.setName(originName);
+                db.registerTable(newViewTbl);
+
+                LOG.info(
+                        "restore with replace view {} name to {}, origin 
view={}"
+                                + " isAtomicRestore: {}",
+                        newViewTbl.getId(), originName,
+                        originViewTbl == null ? -1L : originViewTbl.getId(),
+                        isAtomicRestore);
+            } finally {
+                newViewTbl.writeUnlock();
+            }
+        }
 
-                View originViewTbl = null;
-                Table originTbl = db.getTableNullable(originName);
-                if (originTbl != null) {
-                    if (originTbl.getType() != TableType.VIEW) {
-                        LOG.warn(
-                                "replace view from {} to {}, but the origin 
view is not VIEW, it type is {}"
-                                        + " isAtomicRestore: {}",
-                                aliasName, originName, originTbl.getType(), 
isAtomicRestore);
-                        return new Status(ErrCode.COMMON_ERROR, "replace view 
failed, the origin view "
-                                + originName + " is not VIEW, it is " + 
originTbl.getType());
-                    }
-                    originViewTbl = (View) originTbl; // save the origin view, 
then drop it.
-                }
+        return Status.OK;
+    }
 
-                // replace the view.
-                View newViewTbl = (View) newTbl;
-                newViewTbl.writeLock();
-                try {
-                    // rename new view name to origin view name and add the 
new view to database.
-                    db.unregisterTable(aliasName);
-                    db.unregisterTable(originName);
-                    newViewTbl.setName(originName);
-                    db.registerTable(newViewTbl);
-
-                    LOG.info(
-                            "restore with replace view {} name to {}, origin 
view={}"
-                                    + " isAtomicRestore: {}",
-                            newViewTbl.getId(), originName,
-                            originViewTbl == null ? -1L : 
originViewTbl.getId(),
-                            isAtomicRestore);
-                } finally {
-                    newViewTbl.writeUnlock();
+    private Status prevalidateAtomicRestoreTargets(Database db) {
+        for (String tableName : jobInfo.backupOlapTableObjects.keySet()) {
+            String originName = restoreTargetName(tableName);
+            String aliasName = tableAliasWithAtomicRestore(originName);
+            Table newTable = db.getTableNullable(aliasName);
+            if (newTable == null) {
+                return new Status(ErrCode.COMMON_ERROR,
+                        "replace table failed, the temp table " + aliasName + 
" is not found");
+            }
+            if (newTable.getType() != TableType.OLAP) {
+                return new Status(ErrCode.COMMON_ERROR, "replace table failed, 
the temp table "
+                        + aliasName + " is not OLAP table, it is " + 
newTable.getType());
+            }
+            try {
+                ((OlapTable) newTable).checkAndSetName(originName, true);
+            } catch (DdlException e) {
+                return new Status(ErrCode.COMMON_ERROR, "replace table failed, 
the temp table "
+                        + aliasName + " cannot be renamed to " + originName
+                        + ", reason=" + e.getMessage());
+            }
+            Table originTable = db.getTableNullable(originName);
+            if (originTable != null && originTable.getType() != 
TableType.OLAP) {
+                return new Status(ErrCode.COMMON_ERROR, "replace table failed, 
the origin table "
+                        + originName + " is not OLAP table, it is " + 
originTable.getType());
+            }
+        }
+        for (BackupJobInfo.BackupViewInfo backupViewInfo : 
jobInfo.newBackupObjects.views) {
+            String originName = restoreTargetName(backupViewInfo.name);
+            String aliasName = tableAliasWithAtomicRestore(originName);
+            Table newView = db.getTableNullable(aliasName);
+            if (newView == null) {
+                return new Status(ErrCode.COMMON_ERROR,
+                        "replace view failed, the temp view " + aliasName + " 
is not found");
+            }
+            if (newView.getType() != TableType.VIEW) {
+                return new Status(ErrCode.COMMON_ERROR, "replace view failed, 
the temp view "
+                        + aliasName + " is not VIEW, it is " + 
newView.getType());
+            }
+            Table originView = db.getTableNullable(originName);
+            if (originView != null && originView.getType() != TableType.VIEW) {
+                return new Status(ErrCode.COMMON_ERROR, "replace view failed, 
the origin view "
+                        + originName + " is not VIEW, it is " + 
originView.getType());
+            }
+        }
+        return Status.OK;
+    }
+
+    private List<TableNameInfo> getAtomicRestoreConstraintDropTargets(Database 
db) {
+        Set<String> tableNames = Sets.newLinkedHashSet();
+        for (String tableName : jobInfo.backupOlapTableObjects.keySet()) {
+            String originName = restoreTargetName(tableName);
+            tableNames.add(originName);
+            tableNames.add(tableAliasWithAtomicRestore(originName));
+        }
+        for (BackupJobInfo.BackupViewInfo view : 
jobInfo.newBackupObjects.views) {
+            String originName = restoreTargetName(view.name);
+            tableNames.add(originName);
+            tableNames.add(tableAliasWithAtomicRestore(originName));
+        }
+        if (isCleanTables) {

Review Comment:
   [P1] Keep clean-table mappings covered by frontend admission
   
   With `clean_tables=true`, this adds every live table to the earlier batch 
constraint drop. That removes a non-restored table's global mapping while its 
concrete table object still retains the mapping and remains live until the 
later drop/recycle loop. `ADD FRONTEND` does not take the database lock, so it 
can pass both the live-manager and recycle-bin scans in that interval; the 
later per-table fence only serializes after the old FE was admitted and does 
not revalidate it. Please hold the admission fence across the batch-to-recycle 
transition, or defer each clean table's mapping removal to its already-fenced 
drop, and add a concurrent clean-restore/admission test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -117,20 +157,111 @@ public void addConstraint(TableNameInfo tableNameInfo, 
String constraintName,
                         tableNameInfo, (ForeignKeyConstraint) constraint);
             }
             tableConstraints.put(constraintName, constraint);
+            if (constraint instanceof DistributionMappingConstraint) {
+                putTableLocalConstraint(table, constraintName, constraint);

Review Comment:
   [P1] Include active backup copies in frontend compatibility admission
   
   Publishing the mapping into `TableAttributes` also makes every active 
`BackupJob.backupMeta` deep-copy carry the new Gson subtype. That job can 
outlive a later live `DROP CONSTRAINT`: after a checkpoint compacts the 
ADD/DROP history, the manager and recycle-bin scans are empty, but 
`BackupHandler.write` still serializes the mapping-bearing active job. An older 
FE can then be admitted and fail loading that image even though no restore 
occurs, so the restore and recycle fixes do not cover this holder. Please keep 
mapping-bearing backup-job state inside the compatibility/admission protocol 
until it is no longer journaled or checkpointed (or use a backward-readable 
representation), with a paused-backup/drop/checkpoint/admission test.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/MetastoreEventSyncDriver.java:
##########
@@ -276,6 +293,8 @@ private void applyOne(PluginDrivenExternalCatalog catalog, 
Connector connector,
             case UNREGISTER_TABLE:
                 catalogMgr.unregisterExternalTableFromEvent(
                         before.localDbName, before.localTableName, 
catalogName);
+                
Env.getCurrentEnv().getConstraintManager().dropTableConstraints(

Review Comment:
   [P1] Invalidate MTMV plans on external structural drops
   
   This new manager transition removes the table's PK/UK/FK proofs, but neither 
the event path nor the parallel explicit/replay plugin drops advance dependent 
MTMV rewrite generations. After the external table is recreated under the same 
qualified name, its generated ID and `BaseTableInfo` identity are name-stable, 
and `getOrGenerateCache` can return the old plan immediately even though the 
proof used to build it is gone. The existing structural-event thread required 
removing the manager entries; it does not make this new cache transition 
happen. Please collect and invalidate dependent MTMVs inside the same 
metadata-mutation interval for table/database drops, with a pre-populated-cache 
drop/recreate test.



-- 
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