zclllyybb commented on code in PR #65476:
URL: https://github.com/apache/doris/pull/65476#discussion_r3632149385


##########
fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java:
##########
@@ -464,8 +491,7 @@ public void dataLoad(ConnectContext ctx, Dictionary 
dictionary, boolean adaptive
         lockRead();
         boolean unlocked = false;
         try {
-            if (!dictionaryIds.containsKey(dictionary.getDbName())
-                    || 
!dictionaryIds.get(dictionary.getDbName()).containsKey(dictionary.getName())) {
+            if (!isCurrentDictionaryWithoutLock(dictionary)) {

Review Comment:
   Fixed in 566e7aa2c88. The failure path no longer logs a version decrement. 
Once a full load may have staged V+1, FE persists V+1 as a monotonic fence and 
leaves the dictionary OUT_OF_DATE, so replay cannot see a post-drop decrement 
and that version is never reused.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertTargetDropRaceTest.java:
##########
@@ -0,0 +1,182 @@
+// 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.Env;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.util.DebugPointUtil;
+import org.apache.doris.dictionary.Dictionary;
+import org.apache.doris.dictionary.Dictionary.DictionaryStatus;
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.OriginStatement;
+import org.apache.doris.qe.StmtExecutor;
+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 InsertTargetDropRaceTest extends TestWithFeService {
+    private static final String DICTIONARY_BLOCK_POINT = 
"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 
normalInsertReturnsAnalysisErrorWhenDatabaseIsDroppedAfterTargetResolution() 
throws Exception {
+        String dbName = "normal_insert_drop_race";
+        createDatabaseAndUse(dbName);
+        createTable("CREATE TABLE target_table (id INT) DISTRIBUTED BY 
HASH(id) BUCKETS 1 "
+                + "PROPERTIES ('replication_num' = '1')");
+
+        String sql = "INSERT INTO " + dbName + ".target_table VALUES (1)";
+        InsertIntoTableCommand baseCommand = (InsertIntoTableCommand) new 
NereidsParser().parseSingle(sql);
+        CountDownLatch targetResolved = new CountDownLatch(1);
+        CountDownLatch resumeInsert = new CountDownLatch(1);
+        AtomicBoolean blockOnce = new AtomicBoolean(true);
+        InsertIntoTableCommand command = new 
InsertIntoTableCommand(baseCommand,
+                PlanType.INSERT_INTO_TABLE_COMMAND) {
+            @Override
+            protected InsertTarget getTarget(ConnectContext ctx, List<String> 
qualifiedTargetTableName) {
+                InsertTarget target = super.getTarget(ctx, 
qualifiedTargetTableName);
+                if (blockOnce.compareAndSet(true, false)) {
+                    targetResolved.countDown();
+                    await(resumeInsert);
+                }
+                return target;
+            }
+        };
+
+        ExecutorService executorService = Executors.newSingleThreadExecutor();
+        try {
+            Future<Throwable> result = executorService.submit(() -> 
runInitPlan(command, sql, dbName));
+            Assertions.assertTrue(targetResolved.await(10, TimeUnit.SECONDS));
+            Env.getCurrentInternalCatalog().dropDb(dbName, false, true);
+            resumeInsert.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.assertFalse(hasCause(failure, 
NullPointerException.class), failure.toString());
+        } finally {
+            resumeInsert.countDown();
+            executorService.shutdownNow();
+        }
+    }
+
+    @Test
+    void dictionaryLoadStopsCleanlyWhenDatabaseIsDroppedBeforePlanning() 
throws Exception {
+        Config.enable_debug_points = true;
+        DebugPointUtil.addDebugPoint(DICTIONARY_BLOCK_POINT);
+
+        String dbName = "dictionary_insert_drop_race";
+        createDatabaseAndUse(dbName);
+        createTable("CREATE TABLE source_table (id INT NOT NULL, city 
VARCHAR(32) NOT NULL) "
+                + "DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES 
('replication_num' = '1')");
+        executeNereidsSql("CREATE DICTIONARY dic1 USING source_table (city 
KEY, id VALUE) "
+                + "LAYOUT(HASH_MAP) PROPERTIES ('data_lifetime' = '600')");
+
+        Dictionary dictionary = 
Env.getCurrentEnv().getDictionaryManager().getDictionary(dbName, "dic1");
+        try {
+            await(() -> dictionary.getStatus() == DictionaryStatus.LOADING);

Review Comment:
   Fixed. The FE race test now waits until the DictionaryManager debug point 
itself has executed before dropping the database, so it exercises the 
captured-target planning window instead of merely observing LOADING.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/AbstractInsertExecutor.java:
##########
@@ -107,8 +108,18 @@ public AbstractInsertExecutor(ConnectContext ctx, TableIf 
table, String labelNam
      */
     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);

Review Comment:
   Fixed. Executor construction now receives the database resolved together 
with the validated table target; it no longer recovers a database through the 
stale Table object. Target identity is checked under the database lock before 
the executor and load job are created.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java:
##########
@@ -308,13 +333,17 @@ public void beforeComplete(AbstractInsertExecutor 
insertExecutor, StmtExecutor e
             }
 
             // lock after plan and check does table's schema changed to ensure 
we lock table order by id.
-            TableIf newestTargetTableIf = getTargetTableIf(ctx, 
qualifiedTargetTableName);
+            InsertTarget newestTarget = getTarget(ctx, 
qualifiedTargetTableName);
+            DatabaseIf<?> newestTargetDatabase = newestTarget.getDatabase();
+            TableIf newestTargetTableIf = newestTarget.getTable();
             newestTargetTableIf.readLock();
             try {
-                if (targetTableIf.getId() != newestTargetTableIf.getId()) {
-                    LOG.warn("insert plan failed {} times. query id is {}. 
table id changed from {} to {}",
+                if (targetDatabase.getId() != newestTargetDatabase.getId()

Review Comment:
   Fixed. Rejected planning attempts restore group-commit state, and an 
identity change now fails the statement instead of retargeting the same 
StatementContext. Schema-only retries reset the per-attempt privilege state 
before replanning.



##########
fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java:
##########
@@ -464,8 +491,7 @@ public void dataLoad(ConnectContext ctx, Dictionary 
dictionary, boolean adaptive
         lockRead();
         boolean unlocked = false;
         try {
-            if (!dictionaryIds.containsKey(dictionary.getDbName())
-                    || 
!dictionaryIds.get(dictionary.getDbName()).containsKey(dictionary.getName())) {
+            if (!isCurrentDictionaryWithoutLock(dictionary)) {

Review Comment:
   Fixed. Cleanup computes the staged version from the load type: partial 
refresh aborts the current version, while full refresh aborts the incremented 
fenced version. Failed partial refreshes are marked OUT_OF_DATE so the next 
refresh is full.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java:
##########
@@ -256,65 +279,62 @@ public AbstractInsertExecutor initPlan(ConnectContext 
ctx, StmtExecutor stmtExec
                                            boolean needBeginTransaction) 
throws Exception {
         List<String> qualifiedTargetTableName = 
InsertUtils.getTargetTableQualified(originLogicalQuery, ctx);
 
-        AbstractInsertExecutor insertExecutor;
+        AbstractInsertExecutor insertExecutor = null;
         int retryTimes = 0;
         ctx.getStatementContext().setIsInsert(true);
         while (++retryTimes < 
Math.max(ctx.getSessionVariable().dmlPlanRetryTimes, 3)) {
-            TableIf targetTableIf = getTargetTableIf(ctx, 
qualifiedTargetTableName);
+            boolean groupCommitBeforeAttempt = ctx.isGroupCommit();
+            Backend groupCommitBackendBeforeAttempt =
+                    ctx.getStatementContext().getGroupCommitMergeBackend();
+            insertExecutor = null;
+            InsertTarget target = getTarget(ctx, qualifiedTargetTableName);
+            DatabaseIf<?> targetDatabase = target.getDatabase();
+            TableIf targetTableIf = target.getTable();
             // 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;
+            ExecutorFactory executorFactory;
             try {
                 // use originLogicalQuery to build logicalQuery again.
-                buildResult = initPlanOnce(ctx, stmtExecutor, targetTableIf);
+                executorFactory = initPlanOnce(ctx, stmtExecutor, 
targetTableIf);
             } catch (Throwable e) {
                 Throwables.throwIfInstanceOf(e, RuntimeException.class);
                 throw new IllegalStateException(e.getMessage(), e);
             }
-            insertExecutor = buildResult.executor;
-            parsedPlan = 
Optional.ofNullable(buildResult.planner.getParsedPlan());
-            Plan analyzedPlan = buildResult.planner.getAnalyzedPlan();
+            parsedPlan = 
Optional.ofNullable(executorFactory.planner.getParsedPlan());
+            Plan analyzedPlan = executorFactory.planner.getAnalyzedPlan();
             lineagePlan = Optional.ofNullable(analyzedPlan);
-            if (!needBeginTransaction) {
-                return insertExecutor;
-            }
 
-            List<TableStreamUpdateInfo> infos = 
StreamConsumptionInfoExtractor.extract(analyzedPlan);
+            List<TableStreamUpdateInfo> infos = needBeginTransaction
+                    ? StreamConsumptionInfoExtractor.extract(analyzedPlan) : 
Lists.newArrayList();
             if (!infos.isEmpty()) {
                 if (!Config.enable_feature_binlog) {
                     throw new AnalysisException("Insert plan with Table stream 
failed."
                             + " should enable binlog feature in FE config.");
                 }
-                // put offset into executor
-                insertExecutor.setStreamUpdateInfos(infos);
-                insertExecutor.registerListener(new InsertExecutorListener() {
-                    @Override
-                    public void beforeComplete(AbstractInsertExecutor 
insertExecutor, StmtExecutor executor,
-                                               long jobId) throws Exception {
-                        TransactionState transactionState = 
Env.getCurrentGlobalTransactionMgr()
-                                
.getTransactionState(insertExecutor.getDatabase().getId(),
-                                        insertExecutor.getTxnId());
-                        
transactionState.setStreamUpdateInfos(insertExecutor.getStreamUpdateInfos());
-                    }
-                });
             }
 
             // lock after plan and check does table's schema changed to ensure 
we lock table order by id.
-            TableIf newestTargetTableIf = getTargetTableIf(ctx, 
qualifiedTargetTableName);
+            InsertTarget newestTarget = getTarget(ctx, 
qualifiedTargetTableName);
+            DatabaseIf<?> newestTargetDatabase = newestTarget.getDatabase();
+            TableIf newestTargetTableIf = newestTarget.getTable();
             newestTargetTableIf.readLock();
             try {
-                if (targetTableIf.getId() != newestTargetTableIf.getId()) {
-                    LOG.warn("insert plan failed {} times. query id is {}. 
table id changed from {} to {}",
+                if (targetDatabase.getId() != newestTargetDatabase.getId()
+                        || targetTableIf.getId() != 
newestTargetTableIf.getId()) {
+                    LOG.warn("insert plan failed {} times. query id is {}. 
target id changed from {}.{} to {}.{}",
                             retryTimes, DebugUtil.printId(ctx.queryId()),
-                            targetTableIf.getId(), 
newestTargetTableIf.getId());
+                            targetDatabase.getId(), targetTableIf.getId(),
+                            newestTargetDatabase.getId(), 
newestTargetTableIf.getId());
+                    ctx.setGroupCommit(groupCommitBeforeAttempt);
+                    
ctx.getStatementContext().setGroupCommitMergeBackend(groupCommitBackendBeforeAttempt);
                     newestTargetTableIf.readUnlock();
                     continue;

Review Comment:
   Fixed by removing identity retargeting from the retry loop. If the target 
database or table ID changes, planning fails and the caller retries with a 
fresh statement; only same-object schema changes retry in place, so cached 
source and target relations cannot cross object generations.



##########
regression-test/suites/dictionary_p0/test_create_drop_sync.groovy:
##########
@@ -81,8 +81,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) {

Review Comment:
   Fixed. The exact captured-database race is now covered by a deterministic FE 
unit test that waits for execution of 
DictionaryManager.dataLoad.blockBeforePlan. The regression test was changed to 
cover the separate post-staging commit/drop race with blockBeforeCommit.



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