This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 8c21bda9fd4 branch-4.1: [fix](fe) Skip DEC journal when dictionary 
dropped during load commit- #66552 (#66724)
8c21bda9fd4 is described below

commit 8c21bda9fd47d4331b9fd53194f8a09c5f9d1833
Author: linrrarity <[email protected]>
AuthorDate: Thu Aug 13 18:54:36 2026 +0800

    branch-4.1: [fix](fe) Skip DEC journal when dictionary dropped during load 
commit- #66552 (#66724)
    
    cherry pick: https://github.com/apache/doris/pull/65476 and
    https://github.com/apache/doris/pull/66552
    
    ---------
    
    Co-authored-by: zclllyybb <[email protected]>
---
 .../apache/doris/dictionary/DictionaryManager.java |  97 +++++++++--
 .../nereids/analyzer/UnboundDictionarySink.java    |  17 +-
 .../nereids/analyzer/UnboundTableSinkCreator.java  |   7 +-
 .../doris/nereids/rules/analysis/BindSink.java     |  18 +-
 .../commands/insert/AbstractInsertExecutor.java    |  12 +-
 .../commands/insert/DictionaryInsertExecutor.java  |   8 +-
 .../insert/InsertIntoDictionaryCommand.java        |  17 +-
 .../commands/insert/InsertIntoTableCommand.java    |  15 +-
 .../doris/dictionary/DictionaryManagerTest.java    | 112 ++++++++++++
 .../insert/DictionaryInsertTargetDropRaceTest.java | 188 +++++++++++++++++++++
 .../dictionary_p0/test_create_drop_sync.groovy     |  36 +++-
 ...t_dictionary_drop_while_load_commit_fail.groovy | 113 +++++++++++++
 12 files changed, 588 insertions(+), 52 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java 
b/fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java
index 7ebc701c32a..b389f4756c1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java
@@ -18,6 +18,7 @@
 package org.apache.doris.dictionary;
 
 import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Database;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.ClientPool;
@@ -27,6 +28,7 @@ import org.apache.doris.common.DdlException;
 import org.apache.doris.common.Status;
 import org.apache.doris.common.io.Text;
 import org.apache.doris.common.io.Writable;
+import org.apache.doris.common.util.DebugPointUtil;
 import org.apache.doris.common.util.MasterDaemon;
 import org.apache.doris.dictionary.Dictionary.DictionaryStatus;
 import org.apache.doris.job.extensions.insert.InsertTask;
@@ -267,6 +269,7 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
             // Log the drop operation
             if (dbDictIds != null) {
                 for (Map.Entry<String, Long> entry : dbDictIds.entrySet()) {
+                    idToDictionary.remove(entry.getValue());
                     Env.getCurrentEnv().getEditLog().logDropDictionary(dbName, 
entry.getKey());
                 }
                 // also drop all name mapping records.
@@ -282,6 +285,37 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
         return dbDictIds != null && dbDictIds.containsKey(dictName);
     }
 
+    public boolean isCurrentDictionary(Database database, Dictionary 
dictionary) {
+        lockRead();
+        try {
+            return isCurrentDictionaryWithoutLock(database, dictionary);
+        } finally {
+            unlockRead();
+        }
+    }
+
+    private boolean isCurrentDictionaryWithoutLock(Database database, 
Dictionary dictionary) {
+        Map<String, Long> dbDictIds = 
dictionaryIds.get(dictionary.getDbName());
+        Long dictionaryId = dbDictIds == null ? null : 
dbDictIds.get(dictionary.getName());
+        return Env.getCurrentInternalCatalog().getDbNullable(database.getId()) 
== database
+                && dictionaryId != null
+                && dictionaryId == dictionary.getId()
+                && idToDictionary.get(dictionary.getId()) == dictionary;
+    }
+
+    private Database getCurrentDatabase(Dictionary dictionary) throws 
AnalysisException {
+        lockRead();
+        try {
+            Database database = 
Env.getCurrentInternalCatalog().getDbNullable(dictionary.getDbName());
+            if (database == null || !isCurrentDictionaryWithoutLock(database, 
dictionary)) {
+                throw new AnalysisException("Dictionary " + 
dictionary.getName() + " has been dropped");
+            }
+            return database;
+        } finally {
+            unlockRead();
+        }
+    }
+
     public Map<String, Dictionary> getDictionaries(String dbName) {
         lockRead();
         try {
@@ -406,12 +440,19 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
             LOG.info("skip adaptive dataLoad of dictionary " + 
dictionary.getName() + ". maybe last load finished.");
             return;
         }
+        // Resolve first so every LOADING refresh carries one exact owner 
generation.
+        Database database = getCurrentDatabase(dictionary);
+
         // use atomic status as a lock.
         if (!dictionary.trySetStatus(Dictionary.DictionaryStatus.LOADING)) {
             throw new AnalysisException("Dictionary " + dictionary.getName() + 
" cannot load now, status is "
                     + dictionary.getStatus().name());
         }
 
+        while 
(DebugPointUtil.isEnable("DictionaryManager.dataLoad.blockBeforePlan")) {
+            Thread.sleep(10);
+        }
+
         if (ctx == null) { // for run with scheduler, not by command.
             // priv check is done in relative(caller) command. so use ADMIN 
here is ok.
             ctx = InsertTask.makeConnectContext(UserIdentity.ADMIN, 
dictionary.getDbName());
@@ -433,7 +474,8 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
             baseCommand.setJobId(DICTIONARY_JOB_ID);
         }
 
-        InsertIntoDictionaryCommand command = new 
InsertIntoDictionaryCommand(baseCommand, dictionary, adaptiveLoad);
+        InsertIntoDictionaryCommand command = new InsertIntoDictionaryCommand(
+                baseCommand, database, dictionary, adaptiveLoad);
 
         // run with sync by status.
         try {
@@ -464,8 +506,7 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
         lockRead();
         boolean unlocked = false;
         try {
-            if (!dictionaryIds.containsKey(dictionary.getDbName())
-                    || 
!dictionaryIds.get(dictionary.getDbName()).containsKey(dictionary.getName())) {
+            if (!isCurrentDictionaryWithoutLock(database, dictionary)) {
                 unlockRead();
                 unlocked = true;
 
@@ -495,11 +536,24 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
             }
         }
 
+        // block here in test to simulate the race: INC journal written, 
commit not done yet.
+        while (DebugPointUtil.isEnable("DictionaryManager.afterIncJournal")) {
+            Thread.sleep(100);
+        }
+
         // commit and check the result. not modify metadata so dont need lock.
         if (!commitNowVersion(ctx, dictionary)) {
             if (!ctx.getStatementContext().isPartialLoadDictionary()) {
                 dictionary.decreaseVersion();
-                
Env.getCurrentEnv().getEditLog().logDictionaryDecVersion(dictionary);
+                // DROP may have removed the dictionary between the INC 
journal and this failed
+                // commit. A DEC journal for a dropped dictionary cannot be 
replayed by name, so
+                // only persist the rollback while the dictionary is still the 
current one.
+                if (isCurrentDictionary(database, dictionary)) {
+                    
Env.getCurrentEnv().getEditLog().logDictionaryDecVersion(dictionary);
+                } else {
+                    LOG.warn("Dictionary {} has been dropped or replaced 
during commit, skip DEC journal",
+                            dictionary.getName());
+                }
             }
             dictionary.trySetStatus(oldStatus);
             abortSpecificVersion(ctx, dictionary, dictionary.getVersion() + 1);
@@ -525,6 +579,9 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
     }
 
     private boolean commitNowVersion(ConnectContext ctx, Dictionary 
dictionary) {
+        if 
(DebugPointUtil.isEnable("DictionaryManager.commitNowVersion.fail")) {
+            return false;
+        }
         // use the same BEs when we get before start loading.
         List<Backend> beList = 
ctx.getStatementContext().getUsedBackendsDistributing();
 
@@ -820,21 +877,33 @@ public class DictionaryManager extends MasterDaemon 
implements Writable {
     }
 
     public void replayIncreaseVersion(DictionaryIncreaseVersionInfo info) 
throws DdlException {
-        String dbName = info.getDictionary().getDbName();
-        String dictName = info.getDictionary().getName();
-        Dictionary dictionary = getDictionary(dbName, dictName);
+        long dictId = info.getDictionary().getId();
+        Dictionary dictionary = getDictionary(dictId);
+        if (dictionary == null) {
+            LOG.warn("Dictionary with id {} does not exist when replaying 
increase version, skip", dictId);
+            return;
+        }
         dictionary.writeLock();
-        dictionary.increaseVersion();
-        dictionary.writeUnlock();
+        try {
+            dictionary.increaseVersion();
+        } finally {
+            dictionary.writeUnlock();
+        }
     }
 
     public void replayDecreaseVersion(DictionaryDecreaseVersionInfo info) 
throws DdlException {
-        String dbName = info.getDictionary().getDbName();
-        String dictName = info.getDictionary().getName();
-        Dictionary dictionary = getDictionary(dbName, dictName);
+        long dictId = info.getDictionary().getId();
+        Dictionary dictionary = getDictionary(dictId);
+        if (dictionary == null) {
+            LOG.warn("Dictionary with id {} does not exist when replaying 
decrease version, skip", dictId);
+            return;
+        }
         dictionary.writeLock();
-        dictionary.decreaseVersion();
-        dictionary.writeUnlock();
+        try {
+            dictionary.decreaseVersion();
+        } finally {
+            dictionary.writeUnlock();
+        }
     }
 
     // Metadata serialization
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundDictionarySink.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundDictionarySink.java
index 5d6cc853b10..2750c5e837d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundDictionarySink.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundDictionarySink.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.nereids.analyzer;
 
+import org.apache.doris.catalog.Database;
 import org.apache.doris.dictionary.Dictionary;
 import org.apache.doris.nereids.exceptions.UnboundException;
 import org.apache.doris.nereids.memo.GroupExpression;
@@ -45,15 +46,16 @@ import java.util.Optional;
 public class UnboundDictionarySink<CHILD_TYPE extends Plan> extends 
UnboundLogicalSink<CHILD_TYPE>
         implements Unbound, Sink, BlockFuncDepsPropagation {
 
+    private final Database database;
     private final Dictionary dictionary;
     private final boolean allowAdaptiveLoad;
 
     /**
      * create unbound sink for dictionary sink
      */
-    public UnboundDictionarySink(Dictionary dictionary, CHILD_TYPE child, 
boolean adaptiveLoad) {
+    public UnboundDictionarySink(Database database, Dictionary dictionary, 
CHILD_TYPE child, boolean adaptiveLoad) {
         // all the empty arguments is like UnboundTableSink
-        
super(ImmutableList.copyOf(dictionary.getNameWithFullQualifiers().split("\\.")),
 // nameParts
+        super(ImmutableList.of(database.getCatalog().getName(), 
database.getFullName(), dictionary.getName()),
                 PlanType.LOGICAL_UNBOUND_DICTIONARY_SINK, // type
                 ImmutableList.of(), // outputExprs
                 Optional.empty(), // groupExpression
@@ -61,10 +63,15 @@ public class UnboundDictionarySink<CHILD_TYPE extends Plan> 
extends UnboundLogic
                 dictionary.getColumnNames(), // colNames from dictionary
                 DMLCommandType.INSERT, // dmlCommandType
                 child);
+        this.database = database;
         this.dictionary = dictionary;
         this.allowAdaptiveLoad = adaptiveLoad;
     }
 
+    public Database getDatabase() {
+        return database;
+    }
+
     public Dictionary getDictionary() {
         return dictionary;
     }
@@ -76,7 +83,7 @@ public class UnboundDictionarySink<CHILD_TYPE extends Plan> 
extends UnboundLogic
     @Override
     public Plan withChildren(List<Plan> children) {
         Preconditions.checkArgument(children.size() == 1, 
"UnboundDictionarySink only accepts one child");
-        return new UnboundDictionarySink<>(dictionary, children.get(0), 
allowAdaptiveLoad);
+        return new UnboundDictionarySink<>(database, dictionary, 
children.get(0), allowAdaptiveLoad);
     }
 
     @Override
@@ -91,13 +98,13 @@ public class UnboundDictionarySink<CHILD_TYPE extends Plan> 
extends UnboundLogic
 
     @Override
     public Plan withGroupExpression(Optional<GroupExpression> groupExpression) 
{
-        return new UnboundDictionarySink<>(dictionary, child(), 
allowAdaptiveLoad);
+        return new UnboundDictionarySink<>(database, dictionary, child(), 
allowAdaptiveLoad);
     }
 
     @Override
     public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> 
groupExpression,
             Optional<LogicalProperties> logicalProperties, List<Plan> 
children) {
-        return new UnboundDictionarySink<>(dictionary, children.get(0), 
allowAdaptiveLoad);
+        return new UnboundDictionarySink<>(database, dictionary, 
children.get(0), allowAdaptiveLoad);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
index 1c5b5bf6064..c25b771daa3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.nereids.analyzer;
 
+import org.apache.doris.catalog.Database;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.common.UserException;
 import org.apache.doris.datasource.CatalogIf;
@@ -182,8 +183,8 @@ public class UnboundTableSinkCreator {
     /**
      * create unbound sink for dictionary sink
      */
-    public static UnboundDictionarySink<? extends Plan> 
createUnboundDictionarySink(Dictionary dictionary,
-            LogicalPlan child, boolean adaptiveLoad) {
-        return new UnboundDictionarySink<>(dictionary, child, adaptiveLoad);
+    public static UnboundDictionarySink<? extends Plan> 
createUnboundDictionarySink(Database database,
+            Dictionary dictionary, LogicalPlan child, boolean adaptiveLoad) {
+        return new UnboundDictionarySink<>(database, dictionary, child, 
adaptiveLoad);
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
index 17f7f7208a7..c6053e8b8c3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
@@ -1128,9 +1128,8 @@ public class BindSink implements AnalysisRuleFactory {
 
     private Plan 
bindDictionarySink(MatchingContext<UnboundDictionarySink<Plan>> ctx) {
         UnboundDictionarySink<?> sink = ctx.root;
-        Pair<Database, Dictionary> pair = bind(ctx.cascadesContext, sink);
-        Database database = pair.first;
-        Dictionary dictionary = pair.second;
+        Database database = sink.getDatabase();
+        Dictionary dictionary = sink.getDictionary();
         LogicalPlan child = ((LogicalPlan) sink.child());
 
         // 1. bind target columns: from sink's column names to target tables' 
Columns
@@ -1266,19 +1265,6 @@ public class BindSink implements AnalysisRuleFactory {
         throw new AnalysisException("the target table of insert into is not an 
jdbc table");
     }
 
-    private Pair<Database, Dictionary> bind(CascadesContext cascadesContext,
-            UnboundDictionarySink<? extends Plan> sink) {
-        Dictionary dictionary = sink.getDictionary();
-        Database db;
-        try {
-            db = 
cascadesContext.getConnectContext().getEnv().getInternalCatalog()
-                    
.getDbOrAnalysisException(dictionary.getDatabase().getName());
-        } catch (org.apache.doris.common.AnalysisException e) {
-            throw new AnalysisException(e.getMessage());
-        }
-        return Pair.of(db, dictionary);
-    }
-
     private List<Long> bindPartitionIds(OlapTable table, List<String> 
partitions, boolean temp) {
         return partitions.isEmpty()
                 ? ImmutableList.of()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/AbstractInsertExecutor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/AbstractInsertExecutor.java
index 8c0ac00b961..b8c34291c93 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/AbstractInsertExecutor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/AbstractInsertExecutor.java
@@ -46,6 +46,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -105,8 +106,17 @@ public abstract class AbstractInsertExecutor {
      */
     public AbstractInsertExecutor(ConnectContext ctx, TableIf table, String 
labelName, NereidsPlanner planner,
             Optional<InsertCommandContext> insertCtx, boolean emptyInsert, 
long jobId, boolean needRegister) {
+        this(ctx, table.getDatabase(), table, labelName, planner, insertCtx, 
emptyInsert, jobId, needRegister);
+    }
+
+    /**
+     * Dictionary loads must retain the owner resolved before a concurrent 
database drop.
+     */
+    public AbstractInsertExecutor(ConnectContext ctx, DatabaseIf<?> database, 
TableIf table, String labelName,
+            NereidsPlanner planner, Optional<InsertCommandContext> insertCtx, 
boolean emptyInsert, long jobId,
+            boolean needRegister) {
         this.ctx = ctx;
-        this.database = table.getDatabase();
+        this.database = Objects.requireNonNull(database, "database should not 
be null");
         this.insertLoadJob = new InsertLoadJob(database.getId(), labelName, 
jobId);
         if (needRegister) {
             ctx.getEnv().getLoadManager().addLoadJob(insertLoadJob);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertExecutor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertExecutor.java
index d29a3350ba3..f045a4c19b4 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertExecutor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertExecutor.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.nereids.trees.plans.commands.insert;
 
+import org.apache.doris.catalog.DatabaseIf;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.util.DebugUtil;
@@ -42,9 +43,10 @@ public class DictionaryInsertExecutor extends 
AbstractInsertExecutor {
     /**
      * constructor
      */
-    public DictionaryInsertExecutor(ConnectContext ctx, Dictionary dictionary, 
String labelName, NereidsPlanner planner,
-            Optional<InsertCommandContext> insertCtx, boolean emptyInsert, 
long jobId) {
-        super(ctx, dictionary, labelName, planner, insertCtx, emptyInsert, 
jobId);
+    public DictionaryInsertExecutor(ConnectContext ctx, DatabaseIf<?> 
database, Dictionary dictionary,
+            String labelName, NereidsPlanner planner, 
Optional<InsertCommandContext> insertCtx, boolean emptyInsert,
+            long jobId) {
+        super(ctx, database, dictionary, labelName, planner, insertCtx, 
emptyInsert, jobId, false);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoDictionaryCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoDictionaryCommand.java
index 45615f62c5b..c589300c545 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoDictionaryCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoDictionaryCommand.java
@@ -18,6 +18,8 @@
 package org.apache.doris.nereids.trees.plans.commands.insert;
 
 import org.apache.doris.analysis.RedirectStatus;
+import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.DatabaseIf;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.dictionary.Dictionary;
 import org.apache.doris.nereids.analyzer.UnboundDictionarySink;
@@ -35,19 +37,22 @@ import java.util.List;
  * logic of InsertIntoTableCommand to maximize code reuse.
  */
 public class InsertIntoDictionaryCommand extends InsertIntoTableCommand {
+    private final Database database;
     private final Dictionary dictionary;
 
     /**
      * Constructor for InsertIntoDictionaryCommand.
      *
      * @param baseCommand The base InsertIntoTableCommand to copy from
+     * @param database The retained owner of the target dictionary
      * @param dictionary The target dictionary to insert into
      * @param adaptiveLoad see DictionaryManager.submitDataLoad
      * @throws AnalysisException if the logical query is not a valid sink
      */
-    public InsertIntoDictionaryCommand(InsertIntoTableCommand baseCommand, 
Dictionary dictionary,
+    public InsertIntoDictionaryCommand(InsertIntoTableCommand baseCommand, 
Database database, Dictionary dictionary,
             boolean adaptiveLoad) {
         super(baseCommand, PlanType.INSERT_INTO_DICTIONARY_COMMAND);
+        this.database = database;
         this.dictionary = dictionary;
 
         // Change sink type from olap table(need check) to dictionary
@@ -57,7 +62,7 @@ public class InsertIntoDictionaryCommand extends 
InsertIntoTableCommand {
         }
 
         UnboundTableSink<?> sink = (UnboundTableSink<?>) logicalQuery;
-        UnboundDictionarySink<?> newSink = 
UnboundTableSinkCreator.createUnboundDictionarySink(dictionary,
+        UnboundDictionarySink<?> newSink = 
UnboundTableSinkCreator.createUnboundDictionarySink(database, dictionary,
                 (LogicalPlan) sink.child(0), adaptiveLoad);
         setLogicalQuery(newSink);
         setOriginLogicalQuery(newSink);
@@ -71,9 +76,17 @@ public class InsertIntoDictionaryCommand extends 
InsertIntoTableCommand {
 
     @Override
     protected TableIf getTargetTableIf(ConnectContext ctx, List<String> 
qualifiedTargetTableName) {
+        if (!ctx.getEnv().getDictionaryManager().isCurrentDictionary(database, 
dictionary)) {
+            throw new AnalysisException("Dictionary " + dictionary.getName() + 
" has been dropped");
+        }
         return dictionary;
     }
 
+    @Override
+    protected DatabaseIf<?> getTargetDatabase(TableIf targetTable) {
+        return database;
+    }
+
     public Dictionary getDictionary() {
         return dictionary;
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java
index 5db7a45f8a9..7618f7801e0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java
@@ -20,6 +20,7 @@ package org.apache.doris.nereids.trees.plans.commands.insert;
 import org.apache.doris.analysis.RedirectStatus;
 import org.apache.doris.analysis.StmtType;
 import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.DatabaseIf;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.TableIf;
@@ -239,6 +240,10 @@ public class InsertIntoTableCommand extends Command 
implements NeedAuditEncrypti
         return RelationUtil.getTable(qualifiedTargetTableName, ctx.getEnv(), 
Optional.empty());
     }
 
+    protected DatabaseIf<?> getTargetDatabase(TableIf targetTable) {
+        return targetTable.getDatabase();
+    }
+
     public AbstractInsertExecutor initPlan(ConnectContext ctx, StmtExecutor 
executor) throws Exception {
         return initPlan(ctx, executor, true);
     }
@@ -260,14 +265,15 @@ public class InsertIntoTableCommand extends Command 
implements NeedAuditEncrypti
         ctx.getStatementContext().setIsInsert(true);
         while (++retryTimes < 
Math.max(ctx.getSessionVariable().dmlPlanRetryTimes, 3)) {
             TableIf targetTableIf = getTargetTableIf(ctx, 
qualifiedTargetTableName);
+            DatabaseIf<?> targetDatabase = getTargetDatabase(targetTableIf);
             // check auth
             if (needAuthCheck(targetTableIf) && 
!Env.getCurrentEnv().getAccessManager()
-                    .checkTblPriv(ConnectContext.get(), 
targetTableIf.getDatabase().getCatalog().getName(),
-                            targetTableIf.getDatabase().getFullName(), 
targetTableIf.getName(),
+                    .checkTblPriv(ConnectContext.get(), 
targetDatabase.getCatalog().getName(),
+                            targetDatabase.getFullName(), 
targetTableIf.getName(),
                             PrivPredicate.LOAD)) {
                 
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, 
"LOAD",
                         ConnectContext.get().getQualifiedUser(), 
ConnectContext.get().getRemoteIP(),
-                        targetTableIf.getDatabase().getFullName()
+                        targetDatabase.getFullName()
                                 + "." + 
Util.getTempTableDisplayName(targetTableIf.getName()));
             }
             BuildInsertExecutorResult buildResult;
@@ -559,10 +565,11 @@ public class InsertIntoTableCommand extends Command 
implements NeedAuditEncrypti
             } else if (physicalSink instanceof PhysicalDictionarySink) {
                 boolean emptyInsert = childIsEmptyRelation(physicalSink);
                 Dictionary dictionary = (Dictionary) targetTableIf;
+                DatabaseIf<?> database = getTargetDatabase(dictionary);
                 // insertCtx is not useful for dictionary. so keep it empty is 
ok.
                 return ExecutorFactory.from(planner, dataSink, physicalSink,
                         () -> new DictionaryInsertExecutor(
-                                ctx, dictionary, label, planner, insertCtx, 
emptyInsert, jobId));
+                                ctx, database, dictionary, label, planner, 
insertCtx, emptyInsert, jobId));
             } else if (physicalSink instanceof PhysicalBlackholeSink) {
                 boolean emptyInsert = childIsEmptyRelation(physicalSink);
                 // insertCtx is not useful for blackhole. so keep it empty is 
ok.
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/dictionary/DictionaryManagerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/dictionary/DictionaryManagerTest.java
new file mode 100644
index 00000000000..4fdb9a4ee38
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/dictionary/DictionaryManagerTest.java
@@ -0,0 +1,112 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.dictionary;
+
+import org.apache.doris.persist.CreateDictionaryPersistInfo;
+import org.apache.doris.persist.DictionaryDecreaseVersionInfo;
+import org.apache.doris.persist.DictionaryIncreaseVersionInfo;
+import org.apache.doris.persist.DropDictionaryPersistInfo;
+import org.apache.doris.persist.gson.GsonUtils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests for dictionary version journal replay robustness.
+ *
+ * The crash in production: an async data load task writes the INC journal, 
then DROP removes the
+ * dictionary, then the failed commit writes a DEC journal for the 
already-dropped dictionary.
+ * Followers crash at replay because the dictionary cannot be found by name 
anymore.
+ * Replay must be idempotent and lookup by dictionary id.
+ */
+public class DictionaryManagerTest {
+
+    private DictionaryManager createManager() {
+        return new DictionaryManager();
+    }
+
+    private Dictionary buildDictionary(long id, String dbName, String 
dictName, long version) {
+        String json = String.format(
+                
"{\"clazz\":\"Dictionary\",\"id\":%d,\"name\":\"%s\",\"dbName\":\"%s\","
+                        + "\"sourceTableName\":\"src_%s\",\"version\":%d}",
+                id, dictName, dbName, dbName, version);
+        return GsonUtils.GSON.fromJson(json, Dictionary.class);
+    }
+
+    @Test
+    public void testReplayDecreaseVersionMissingDictionary() throws Exception {
+        DictionaryManager manager = createManager();
+        // dictionary never created on this FE
+        Dictionary dict = buildDictionary(1001, "db1", "dic1", 2);
+        manager.replayDecreaseVersion(new DictionaryDecreaseVersionInfo(dict));
+    }
+
+    @Test
+    public void testReplayIncreaseVersionMissingDictionary() throws Exception {
+        DictionaryManager manager = createManager();
+        Dictionary dict = buildDictionary(1001, "db1", "dic1", 1);
+        manager.replayIncreaseVersion(new DictionaryIncreaseVersionInfo(dict));
+    }
+
+    @Test
+    public void testReplayDecreaseVersionAfterDrop() throws Exception {
+        DictionaryManager manager = createManager();
+        Dictionary dict = buildDictionary(1001, "db1", "dic1", 2);
+        manager.replayCreateDictionary(new CreateDictionaryPersistInfo(dict));
+        manager.replayDropDictionary(new DropDictionaryPersistInfo("db1", 
"dic1"));
+
+        // journal order CREATE -> INC -> DROP -> DEC, DEC must be a no-op, 
not an exception
+        manager.replayDecreaseVersion(new DictionaryDecreaseVersionInfo(dict));
+        Assert.assertNull(manager.getDictionary(1001));
+    }
+
+    @Test
+    public void testReplayDecreaseVersionAbA() throws Exception {
+        DictionaryManager manager = createManager();
+        Dictionary oldDict = buildDictionary(1001, "db1", "dic1", 2);
+        manager.replayCreateDictionary(new 
CreateDictionaryPersistInfo(oldDict));
+        manager.replayDropDictionary(new DropDictionaryPersistInfo("db1", 
"dic1"));
+        Dictionary newDict = buildDictionary(1002, "db1", "dic1", 1);
+        manager.replayCreateDictionary(new 
CreateDictionaryPersistInfo(newDict));
+
+        // DEC of the dropped dictionary must not affect the recreated 
same-name dictionary
+        manager.replayDecreaseVersion(new 
DictionaryDecreaseVersionInfo(oldDict));
+        Assert.assertEquals(1, newDict.getVersion());
+        Assert.assertEquals(1, manager.getDictionary(1002).getVersion());
+    }
+
+    @Test
+    public void testReplayDecreaseVersionNormal() throws Exception {
+        DictionaryManager manager = createManager();
+        Dictionary dict = buildDictionary(1001, "db1", "dic1", 2);
+        manager.replayCreateDictionary(new CreateDictionaryPersistInfo(dict));
+
+        manager.replayDecreaseVersion(new DictionaryDecreaseVersionInfo(dict));
+        Assert.assertEquals(1, manager.getDictionary(1001).getVersion());
+    }
+
+    @Test
+    public void testReplayIncreaseVersionNormal() throws Exception {
+        DictionaryManager manager = createManager();
+        Dictionary dict = buildDictionary(1001, "db1", "dic1", 1);
+        manager.replayCreateDictionary(new CreateDictionaryPersistInfo(dict));
+
+        manager.replayIncreaseVersion(new DictionaryIncreaseVersionInfo(dict));
+        Assert.assertEquals(2, manager.getDictionary(1001).getVersion());
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertTargetDropRaceTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertTargetDropRaceTest.java
new file mode 100644
index 00000000000..df31a7b4f4e
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/insert/DictionaryInsertTargetDropRaceTest.java
@@ -0,0 +1,188 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.commands.insert;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.util.DebugPointUtil;
+import org.apache.doris.common.util.DebugPointUtil.DebugPoint;
+import org.apache.doris.dictionary.Dictionary;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.OriginStatement;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.thrift.TUniqueId;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+
+class DictionaryInsertTargetDropRaceTest extends TestWithFeService {
+    private static final String BLOCK_BEFORE_PLAN = 
"DictionaryManager.dataLoad.blockBeforePlan";
+
+    private boolean debugPointsEnabled;
+
+    @BeforeEach
+    void saveDebugPointConfig() {
+        debugPointsEnabled = Config.enable_debug_points;
+    }
+
+    @AfterEach
+    void clearDebugPoints() {
+        DebugPointUtil.clearDebugPoints();
+        Config.enable_debug_points = debugPointsEnabled;
+    }
+
+    @Test
+    void rejectsDroppedDictionaryAfterResolvingItsOwner() throws Exception {
+        Config.enable_debug_points = true;
+        DebugPoint blockPoint = new DebugPoint();
+        blockPoint.executeLimit = Integer.MAX_VALUE;
+        DebugPointUtil.addDebugPoint(BLOCK_BEFORE_PLAN, blockPoint);
+
+        String sourceDbName = "dictionary_insert_drop_race_source";
+        createDatabaseAndUse(sourceDbName);
+        createSourceTable();
+
+        String dbName = "dictionary_insert_drop_race";
+        createDatabaseAndUse(dbName);
+        executeNereidsSql("CREATE DICTIONARY dic1 USING internal." + 
sourceDbName
+                + ".source_table (city KEY, id VALUE) "
+                + "LAYOUT(HASH_MAP) PROPERTIES ('data_lifetime' = '600')");
+
+        Dictionary dictionary = 
Env.getCurrentEnv().getDictionaryManager().getDictionary(dbName, "dic1");
+        Database database = 
Env.getCurrentInternalCatalog().getDbOrDdlException(dbName);
+        String sql = "INSERT INTO " + dbName + ".dic1 SELECT * FROM "
+                + dictionary.getSourceCtlName() + "." + 
dictionary.getSourceDbName() + "."
+                + dictionary.getSourceTableName();
+        InsertIntoTableCommand baseCommand = (InsertIntoTableCommand) new 
NereidsParser().parseSingle(sql);
+        CountDownLatch targetValidated = new CountDownLatch(1);
+        CountDownLatch resumePlanning = new CountDownLatch(1);
+        AtomicBoolean blockOnce = new AtomicBoolean(true);
+        InsertIntoDictionaryCommand command = new InsertIntoDictionaryCommand(
+                baseCommand, database, dictionary, false) {
+            @Override
+            protected TableIf getTargetTableIf(ConnectContext ctx, 
List<String> qualifiedTargetTableName) {
+                TableIf target = super.getTargetTableIf(ctx, 
qualifiedTargetTableName);
+                if (blockOnce.compareAndSet(true, false)) {
+                    targetValidated.countDown();
+                    await(resumePlanning);
+                }
+                return target;
+            }
+        };
+
+        ExecutorService executorService = Executors.newSingleThreadExecutor();
+        try {
+            await(() -> blockPoint.executeNum.get() > 0);
+            Future<Throwable> result = executorService.submit(() -> 
runInitPlan(command, sql, dbName));
+            Assertions.assertTrue(targetValidated.await(10, TimeUnit.SECONDS));
+            Env.getCurrentInternalCatalog().dropDb(dbName, false, true);
+            resumePlanning.countDown();
+
+            Throwable failure = result.get(10, TimeUnit.SECONDS);
+            Assertions.assertNotNull(failure);
+            Assertions.assertTrue(hasCause(failure, 
org.apache.doris.nereids.exceptions.AnalysisException.class),
+                    failure.toString());
+            Assertions.assertTrue(failure.toString().contains("Dictionary dic1 
has been dropped"),
+                    failure.toString());
+            Assertions.assertFalse(hasCause(failure, 
NullPointerException.class), failure.toString());
+            
Assertions.assertNull(Env.getCurrentEnv().getDictionaryManager().getDictionary(dictionary.getId()));
+
+            createDatabaseAndUse(dbName);
+            executeNereidsSql("CREATE DICTIONARY dic1 USING internal." + 
sourceDbName
+                    + ".source_table (city KEY, id VALUE) "
+                    + "LAYOUT(HASH_MAP) PROPERTIES ('data_lifetime' = '600')");
+            Dictionary replacement = 
Env.getCurrentEnv().getDictionaryManager().getDictionary(dbName, "dic1");
+            Assertions.assertNotEquals(dictionary.getId(), 
replacement.getId());
+        } finally {
+            resumePlanning.countDown();
+            DebugPointUtil.removeDebugPoint(BLOCK_BEFORE_PLAN);
+            executorService.shutdownNow();
+        }
+
+        await(() -> !dictionary.getLastUpdateResult().isEmpty());
+        Assertions.assertTrue(dictionary.getLastUpdateResult().contains("has 
been dropped"),
+                dictionary.getLastUpdateResult());
+        
Assertions.assertFalse(dictionary.getLastUpdateResult().contains("Cannot 
invoke"),
+                dictionary.getLastUpdateResult());
+        Env.getCurrentInternalCatalog().dropDb(dbName, false, true);
+        Env.getCurrentInternalCatalog().dropDb(sourceDbName, false, true);
+    }
+
+    private void createSourceTable() throws Exception {
+        createTable("CREATE TABLE source_table (id INT NOT NULL, city 
VARCHAR(32) NOT NULL) "
+                + "DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES 
('replication_num' = '1')");
+    }
+
+    private Throwable runInitPlan(InsertIntoDictionaryCommand command, String 
sql, String dbName) {
+        try {
+            ConnectContext ctx = createCtx(UserIdentity.ROOT, "127.0.0.1");
+            ctx.setDatabase(dbName);
+            ctx.setQueryId(new TUniqueId(1, 2));
+            ctx.setStatementContext(new StatementContext(ctx, new 
OriginStatement(sql, 0)));
+            command.initPlan(ctx, new StmtExecutor(ctx, sql));
+            return null;
+        } catch (Throwable t) {
+            return t;
+        }
+    }
+
+    private static boolean hasCause(Throwable throwable, Class<? extends 
Throwable> causeClass) {
+        for (Throwable cause = throwable; cause != null; cause = 
cause.getCause()) {
+            if (causeClass.isInstance(cause)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void await(CountDownLatch latch) {
+        try {
+            if (!latch.await(10, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("Timed out waiting to resume 
dictionary planning");
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static void await(BooleanSupplier condition) throws 
InterruptedException {
+        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+        while (!condition.getAsBoolean() && System.nanoTime() < deadline) {
+            Thread.sleep(10);
+        }
+        Assertions.assertTrue(condition.getAsBoolean());
+    }
+}
diff --git a/regression-test/suites/dictionary_p0/test_create_drop_sync.groovy 
b/regression-test/suites/dictionary_p0/test_create_drop_sync.groovy
index 951e2bef382..2bd885ec927 100644
--- a/regression-test/suites/dictionary_p0/test_create_drop_sync.groovy
+++ b/regression-test/suites/dictionary_p0/test_create_drop_sync.groovy
@@ -15,7 +15,7 @@
  // specific language governing permissions and limitations
  // under the License.
 
-suite('test_create_drop_sync') {
+suite('test_create_drop_sync', 'nonConcurrent') {
     sql "DROP DATABASE IF EXISTS test_create_drop_sync"
     sql "CREATE DATABASE test_create_drop_sync"
     sql "USE test_create_drop_sync"
@@ -37,6 +37,7 @@ suite('test_create_drop_sync') {
         DISTRIBUTED BY HASH(id) BUCKETS 1
         PROPERTIES("replication_num" = "1")
     """
+    sql "INSERT INTO source_table VALUES (1, 'beijing', '001')"
 
     // create dictionary
     sql """
@@ -81,8 +82,35 @@ suite('test_create_drop_sync') {
         properties('data_lifetime'='600');
     """
 
-    // drop and recreate the database. check dic1 is dropped.
-    sql "DROP DATABASE test_create_drop_sync"
+    waitAllDictionariesReady()
+
+    def refreshFuture
+    try {
+        
GetDebugPoint().enableDebugPointForAllFEs('DictionaryManager.dataLoad.blockBeforePlan')
+        refreshFuture = thread {
+            sql "REFRESH DICTIONARY test_create_drop_sync.dic1"
+        }
+        awaitUntil(10) {
+            def dictionaries = sql "SHOW DICTIONARIES"
+            dictionaries.size() == 1 && dictionaries[0][4] == "LOADING"
+        }
+        sql "DROP DATABASE test_create_drop_sync"
+    } finally {
+        
GetDebugPoint().disableDebugPointForAllFEs('DictionaryManager.dataLoad.blockBeforePlan')
+    }
+
+    Exception refreshFailure = null
+    assertNotNull(refreshFuture)
+    try {
+        refreshFuture.get()
+    } catch (Exception e) {
+        refreshFailure = e
+    }
+    assertNotNull(refreshFailure)
+    assertTrue(refreshFailure.toString().contains("Dictionary dic1 has been 
dropped"), refreshFailure.toString())
+    assertFalse(refreshFailure.toString().contains("Cannot invoke"), 
refreshFailure.toString())
+
+    // Recreate the database and verify that the dropped dictionary is not 
retained.
     sql "CREATE DATABASE test_create_drop_sync"
     sql "USE test_create_drop_sync"
     dict_res = sql "SHOW DICTIONARIES"
@@ -105,4 +133,4 @@ suite('test_create_drop_sync') {
         LAYOUT(HASH_MAP)
         properties('data_lifetime'='600');
     """
-}
\ No newline at end of file
+}
diff --git 
a/regression-test/suites/dictionary_p0/test_dictionary_drop_while_load_commit_fail.groovy
 
b/regression-test/suites/dictionary_p0/test_dictionary_drop_while_load_commit_fail.groovy
new file mode 100644
index 00000000000..77e117a4866
--- /dev/null
+++ 
b/regression-test/suites/dictionary_p0/test_dictionary_drop_while_load_commit_fail.groovy
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.apache.doris.regression.suite.ClusterOptions
+
+// Regress the race reported in DORIS-27820: an async dictionary load task 
writes the INC version
+// journal, then DROP deletes the dictionary, then the failed BE commit writes 
a DEC journal for
+// the already dropped dictionary, making all followers exit when replaying it.
+//
+// With the fix, the DEC journal is skipped when the dictionary was dropped 
during commit, and
+// replay of DEC journals of dropped dictionaries is a no-op, so the whole 
journal stream
+// (CREATE -> INC -> DROP) replays cleanly and FEs stay healthy after restart.
+suite('test_dictionary_drop_while_load_commit_fail', 'docker') {
+    def options = new ClusterOptions()
+    options.cloudMode = false
+    options.feNum = 3
+    options.beNum = 1
+    options.enableDebugPoints()
+
+    docker(options) {
+        sql "drop database if exists test_dictionary_drop_race"
+        sql "create database test_dictionary_drop_race"
+        sql "use test_dictionary_drop_race"
+
+        sql """
+            create table source_table(
+                k1 varchar(100) not null,
+                v1 int not null
+            )
+            DISTRIBUTED BY HASH(`k1`) BUCKETS 1
+            properties("replication_num" = "1");
+        """
+        sql "insert into source_table values ('k1', 1), ('k2', 2), ('k3', 3)"
+
+        // Block the load task right after the INC journal is written, before 
BE commit.
+        
GetDebugPoint().enableDebugPointForAllFEs("DictionaryManager.afterIncJournal")
+        try {
+            sql """
+                create dictionary dict1 using source_table
+                (
+                    k1 KEY,
+                    v1 VALUE
+                )LAYOUT(HASH_MAP)
+                properties('data_lifetime'='600');
+            """
+
+            // wait until the load task is parked at the block point.
+            // status is LOADING before the block, and the task cannot pass 
the block, so once we
+            // observe LOADING for a grace period, INC journal is guaranteed 
already written.
+            boolean loading = false
+            for (int i = 0; i < 40; i++) {
+                def res = sql "SHOW DICTIONARIES"
+                if (res.size() == 1 && res[0][4] == "LOADING") {
+                    loading = true
+                    break
+                }
+                sleep(500)
+            }
+            assertTrue(loading)
+            sleep(1500)
+
+            // DROP the dictionary while the load task is between INC journal 
and commit
+            sql "drop dictionary dict1"
+            def dictRes = sql "SHOW DICTIONARIES"
+            assertEquals(dictRes.size(), 0)
+
+            // force the BE commit to fail, then release the blocked load task
+            
GetDebugPoint().enableDebugPointForAllFEs("DictionaryManager.commitNowVersion.fail")
+            
GetDebugPoint().disableDebugPointForAllFEs("DictionaryManager.afterIncJournal")
+            sleep(3000)
+
+            // master must stay healthy: no DEC journal should have been 
written after DROP
+            assertTrue(cluster.getMasterFe().alive)
+
+            // restart the master: it must replay the whole journal stream 
without exit.
+            // if a DEC journal for the dropped dictionary had been written, 
replay would throw
+            // and the FE would never come back alive.
+            def master = cluster.getMasterFe()
+            cluster.restartFrontends(master.index)
+            boolean hasRestart = false
+            for (int i = 0; i < 60; i++) {
+                if (cluster.getFeByIndex(master.index).alive) {
+                    hasRestart = true
+                    break
+                }
+                sleep(1000)
+            }
+            assertTrue(hasRestart)
+
+            context.reconnectFe()
+            sql "use test_dictionary_drop_race"
+            def finalRes = sql "SHOW DICTIONARIES"
+            assertEquals(finalRes.size(), 0)
+        } finally {
+            
GetDebugPoint().disableDebugPointForAllFEs("DictionaryManager.commitNowVersion.fail")
+            
GetDebugPoint().disableDebugPointForAllFEs("DictionaryManager.afterIncJournal")
+        }
+    }
+}


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

Reply via email to