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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java:
##########
@@ -354,6 +437,45 @@ public void beforeComplete(AbstractInsertExecutor 
insertExecutor, StmtExecutor e
         throw new AnalysisException("Insert plan failed. Could not get target 
table lock.");
     }
 
+    private void lockCurrentTarget(ConnectContext ctx, DatabaseIf<?> database, 
TableIf table) {
+        if (!(database instanceof Database) || table instanceof Dictionary) {
+            table.readLock();
+            return;
+        }
+
+        // Internal DROP uses Database -> Table, so this order closes the 
lookup-to-lock race without inversion.
+        database.readLock();
+        try {
+            boolean current = false;
+            table.readLock();
+            try {
+                Database internalDatabase = (Database) database;
+                current = 
ctx.getEnv().getInternalCatalog().getDbNullable(internalDatabase.getId()) == 
internalDatabase
+                        && internalDatabase.getTableNullable(table.getId()) == 
table;
+            } finally {
+                if (!current) {
+                    table.readUnlock();
+                }
+            }
+            if (current) {
+                return;
+            }
+            LOG.warn("insert target changed before validation. query id is {}. 
target id is {}.{}",
+                    DebugUtil.printId(ctx.queryId()), database.getId(), 
table.getId());
+            throw new AnalysisException("Insert target changed while planning. 
Please retry the statement");
+        } finally {
+            database.readUnlock();
+        }
+    }
+
+    public GroupCommitTarget acquireGroupCommitTarget(ConnectContext ctx) 
throws Exception {
+        OlapTable table = (OlapTable) getTable(ctx);
+        Database database = (Database) table.getDatabase();

Review Comment:
   `getTable()` resolves and authorizes one database/table pair, but this 
helper discards its database and calls `table.getDatabase()`, which is a fresh 
catalog-by-name lookup. If `DROP DATABASE` completes after `getTable()` 
returns, that lookup is null and `acquireGroupCommitAdmission()` immediately 
dereferences it at `database.readLock()`, producing an NPE instead of the 
intended retryable target-changed error. A same-name recreation also 
temporarily pairs the old table with the replacement database. Please carry the 
exact `InsertTarget` pair through authorization and admission, and add a 
DROP-after-authorization test for this command-level path; the current 
replacement test calls the manager directly and cannot catch this gap.



##########
fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java:
##########
@@ -556,45 +645,106 @@ private boolean commitNowVersion(ConnectContext ctx, 
Dictionary dictionary) {
                     if (status.getErrorCode() != TStatusCode.OK) {
                         LOG.warn("Failed to commit dictionary " + 
dictionary.getId() + " on be " + be.getAddress()
                                 + " because " + status.getErrorMsg());
-                        allSucceed = false;
+                        allSucceeded = false;
+                    } else {
+                        anySucceeded = true;
                     }
                 } else {
                     LOG.warn("Failed to commit dictionary " + 
dictionary.getId() + " on be " + be.getAddress());
-                    allSucceed = false;
+                    allSucceeded = false;
+                    outcomeUnknown = true;
                 }
+            } catch (Exception e) {
+                dictionary.setLastUpdateResult("commit failed: " + 
e.getMessage());
+                LOG.warn("Failed to commit dictionary {} on BE {}", 
dictionary.getId(), be.getId(), e);
+                allSucceeded = false;
+                outcomeUnknown = true;
             }
-        } catch (Exception e) {
-            dictionary.setLastUpdateResult("commit failed: " + e.getMessage());
-            LOG.warn("Failed to commit dictionary " + dictionary.getId(), e);
-            allSucceed = false;
         }
-        return allSucceed;
+        return new CommitResult(allSucceeded, anySucceeded, outcomeUnknown);
+    }
+
+    private void abortFailedLoad(ConnectContext ctx, Dictionary dictionary, 
DictionaryStatus oldStatus) {
+        if (ctx.getStatementContext() == null) {
+            dictionary.trySetStatus(oldStatus);
+            return;
+        }
+        boolean partialLoad = 
ctx.getStatementContext().isPartialLoadDictionary();
+        long stagedVersion = partialLoad
+                ? dictionary.getVersion() : dictionary.getVersion() + 1;
+        boolean versionFenced = false;
+        if (!partialLoad && 
ctx.getStatementContext().getUsedBackendsDistributing() != null
+                && 
!ctx.getStatementContext().getUsedBackendsDistributing().isEmpty()) {
+            // Persist the fence before abort so failover cannot reuse a 
version targeted by a delayed RPC.
+            lockRead();
+            try {
+                if (isCurrentDictionary(dictionary)) {
+                    dictionary.increaseVersion();

Review Comment:
   This fence advances the same `Dictionary.version` that `dict_get` sends as 
the committed query version. Starting from a healthy committed V, a full 
refresh that fails after BE assignment increments FE to V+1 and aborts only the 
refreshing entry; every BE can still hold committed V, but 
`DictionaryFactory::get()` requires an exact match, so new queries ask for 
absent V+1 and fail. For an `[INVALID_DICT_MARK]` failure, the source-version 
marker can suppress the automatic retry and make the outage persistent. Please 
keep a separate persisted attempt/high-watermark generation and leave the read 
version at V until commit is known visible; add coverage that begins with 
committed V and proves reads survive a failed refresh.



##########
fe/fe-core/src/main/java/org/apache/doris/dictionary/DictionaryManager.java:
##########
@@ -495,16 +512,41 @@ public void dataLoad(ConnectContext ctx, Dictionary 
dictionary, boolean adaptive
             }
         }
 
+        while 
(DebugPointUtil.isEnable("DictionaryManager.dataLoad.blockBeforeCommit")) {

Review Comment:
   Reaching this post-load point is still the first time a full-refresh 
generation has been journaled; fragments carrying V+1 were already dispatched 
by `command.run()`. If the master dies during that call, replay retains V and 
the new master can dispatch another V+1 while old-master fragments can still 
reach EOS. Since BE `refresh_dict()` unconditionally replaces the single 
refreshing slot for the dictionary ID, old EOS can overwrite newly staged S2 
with S1 at the same V+1; the new FE then commits V+1 and records S2 even though 
S1 was published (potentially on only a subset of BEs). Please reserve a 
separate attempt generation before dispatch or transmit a persisted 
master/attempt epoch that rejects stale EOS, and add an in-flight failover test 
rather than only failing over after dictionaries are ready.



##########
fe/fe-core/src/main/java/org/apache/doris/load/GroupCommitManager.java:
##########
@@ -249,6 +356,10 @@ public Backend selectBackendForGroupCommit(long tableId, 
ConnectContext context)
 
     public long selectBackendForGroupCommitInternal(long tableId, String 
cluster)
             throws LoadException, DdlException {
+        // Followers execute group commit locally, so master-side BE selection 
is their admission barrier.
+        synchronized (admissionLock) {
+            checkGroupCommitAdmission(tableId);

Review Comment:
   This is only a point-in-time check, not the follower/HTTP admission barrier 
the comment claims: it releases `admissionLock` without registering an active 
request. A follower's eventual admission is in that FE's process-local manager, 
and the HTTP redirect/plan-only routes can have no admission spanning the 
returned plan/body handoff. Thus selection can succeed, then master DDL can 
block, see no active request or WAL, switch/drop metadata, and finally the 
paused request can reach the BE with its old plan. The added test covers only 
block-before-selection. Please return/hold a master-visible lease until BE/WAL 
acceptance (or execute through the master), and test selection-before-block for 
follower ordinary/prepared and HTTP paths.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/OlapGroupCommitInsertExecutor.java:
##########
@@ -164,13 +168,14 @@ protected static void analyzeGroupCommit(ConnectContext 
ctx, TableIf table, Logi
 
     @Override
     protected void beforeExec() {
-        if 
(Env.getCurrentEnv().getGroupCommitManager().isBlock(this.table.getId())) {
-            String msg = "insert table " + this.table.getId() + 
GroupCommitPlanner.SCHEMA_CHANGE;
-            LOG.info(msg);
-            throw new AnalysisException(msg);
-        }
         // this is used for old coordinator
         this.coordinator.setGroupCommitBe(groupCommitBackend);
+        try {
+            groupCommitAdmission = Env.getCurrentEnv().getGroupCommitManager()
+                    .acquireGroupCommitAdmission((Database) database, 
(OlapTable) table);

Review Comment:
   Ordinary local group commit reaches this admission only after `initPlan()` 
has selected a BE, built the plan, and finalized the sink. If it pauses after 
`initPlan()` returns, schema change/rollup can block, see no active request or 
WAL, install S2, and unblock; this call then succeeds because admission 
validates only the same database/table objects, while schema change mutates 
that table object in place. The S1 plan is submitted after the very drain 
intended to exclude it, and this path has no prepared-style generation replan. 
Please acquire admission before ordinary group-commit planning/finalization, or 
atomically validate a schema/block epoch here and replan, with a latch between 
`initPlan()` and `beforeExec()`.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/OlapGroupCommitInsertExecutor.java:
##########
@@ -179,31 +184,47 @@ public void beginTransaction() {
 
     @Override
     protected void onComplete() {
-        if (ctx.getState().getStateType() == MysqlStateType.ERR) {
-            txnStatus = TransactionStatus.ABORTED;
-        } else {
-            txnStatus = TransactionStatus.PREPARE;
+        try {
+            if (ctx.getState().getStateType() == MysqlStateType.ERR) {
+                txnStatus = TransactionStatus.ABORTED;
+            } else {
+                txnStatus = TransactionStatus.PREPARE;
+            }
+        } finally {
+            releaseGroupCommitAdmission();
         }
     }
 
     @Override
     protected void onFail(Throwable t) {
-        errMsg = t.getMessage() == null ? "unknown reason" : t.getMessage();
-        String queryId = DebugUtil.printId(ctx.queryId());
-        // if any throwable being thrown during insert operation, first we 
should abort this txn
-        LOG.warn("insert [{}] with query id {} failed, url={}", labelName, 
queryId, coordinator.getTrackingUrl(), t);
-        String firstErrorMsgPart = "";
-        String urlPart = "";
-        if (!Strings.isNullOrEmpty(coordinator.getFirstErrorMsg())) {
-            firstErrorMsgPart = 
StringUtils.abbreviate(coordinator.getFirstErrorMsg(),
-                    Config.first_error_msg_max_length);
-        }
-        if (!Strings.isNullOrEmpty(coordinator.getTrackingUrl())) {
-            urlPart = coordinator.getTrackingUrl();
+        try {
+            errMsg = t.getMessage() == null ? "unknown reason" : 
t.getMessage();
+            String queryId = DebugUtil.printId(ctx.queryId());
+            // if any throwable being thrown during insert operation, first we 
should abort this txn
+            LOG.warn("insert [{}] with query id {} failed, url={}", labelName, 
queryId,
+                    coordinator.getTrackingUrl(), t);
+            String firstErrorMsgPart = "";
+            String urlPart = "";
+            if (!Strings.isNullOrEmpty(coordinator.getFirstErrorMsg())) {
+                firstErrorMsgPart = 
StringUtils.abbreviate(coordinator.getFirstErrorMsg(),
+                        Config.first_error_msg_max_length);
+            }
+            if (!Strings.isNullOrEmpty(coordinator.getTrackingUrl())) {
+                urlPart = coordinator.getTrackingUrl();
+            }
+
+            String finalErrorMsg = InsertUtils.getFinalErrorMsg(errMsg, 
firstErrorMsgPart, urlPart);
+            ctx.getState().setError(ErrorCode.ERR_UNKNOWN_ERROR, 
finalErrorMsg);
+        } finally {
+            releaseGroupCommitAdmission();

Review Comment:
   This releases the admission before remote work is known quiescent. 
`executeSingleInsert()` calls `onFail()` before `coordinator.close()`, and both 
coordinator cancellation paths send their remote cancellation asynchronously. 
After an exec timeout/lost response, DDL can therefore observe the active count 
reach zero and no WAL, finish its schema/drop barrier, and only then have the 
delayed old fragment reach its group-commit sink. The prepared direct-RPC 
sibling has the same issue when `Future.get()` throws and closes its 
try-with-resources admission. Please retain a fence until remote 
cancellation/completion is acknowledged, or add a BE-side generation/tombstone 
that rejects late old requests, with delayed-exec/lost-response races against 
schema change and DROP.



##########
fe/fe-core/src/main/java/org/apache/doris/alter/MaterializedViewHandler.java:
##########
@@ -195,7 +195,10 @@ public void 
processCreateMaterializedView(CreateMaterializedViewCommand createMv
             OlapTable olapTable) throws DdlException, AnalysisException {
         // wait wal delete
         
Env.getCurrentEnv().getGroupCommitManager().blockTable(olapTable.getId());

Review Comment:
   With ref-counted blocking, this acquisition needs per-call ownership 
tracking. Two concurrent CREATE MV/ADD ROLLUP requests can both increment 
before taking the table lock. The winner submits a job and sets `ROLLUP`; the 
loser then acquires the lock and fails `checkNormalStateForAlter()`, but its 
`finally` skips `unblockTable()` because it infers ownership from the winner's 
global `ROLLUP` state. When the winning job completes, it releases only one 
reference, leaving the table blocked forever. The old Set semantics masked this 
imbalance. Please release every failed call's own reference and transfer 
ownership to a registered job explicitly, with a two-request test that verifies 
the count reaches zero after the winner completes.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java:
##########
@@ -1022,6 +1029,26 @@ private void dropTableInternal(Database db, Table table, 
boolean isView, boolean
         }
     }
 
+    private List<Long> blockGroupCommitLoads(Collection<Table> tables) throws 
DdlException {
+        List<Long> tableIds = tables.stream()
+                .filter(OlapTable.class::isInstance)
+                .map(Table::getId)
+                .collect(Collectors.toList());
+        // Rejecting an active load keeps DROP from waiting on remote work 
while metadata locks are held.
+        
tableIds.forEach(Env.getCurrentEnv().getGroupCommitManager()::blockTable);
+        for (long tableId : tableIds) {
+            if 
(Env.getCurrentEnv().getGroupCommitManager().hasActiveGroupCommitLoad(tableId)) 
{

Review Comment:
   This protects only the active-request interval, not an acknowledged async 
request whose admission has closed but whose WAL is still queued. In async mode 
the BE writes/registers WAL and can return success before the batched internal 
transaction commits; at that point `hasActiveGroupCommitLoad()` is false while 
the per-table WAL queue is nonempty. DROP then removes the table/database, and 
the later `loadTxnCommit` fails and can strand the WAL in recovery. Unlike 
schema change and rollup, this path never calls `waitWalFinished()`. Please 
keep the block while draining both admissions and WAL outside metadata locks 
and then revalidate, or reject DROP while either exists; cover both table and 
database DROP with queued async WAL.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertIntoTableCommand.java:
##########
@@ -135,6 +139,50 @@ public class InsertIntoTableCommand extends Command 
implements NeedAuditEncrypti
 
     private InsertExecutorListener insertExecutorListener;
 
+    /** A database and table resolved by the same catalog lookup. */
+    protected static class InsertTarget {
+        private final DatabaseIf<?> database;
+        private final TableIf table;
+
+        protected InsertTarget(DatabaseIf<?> database, TableIf table) {
+            this.database = Objects.requireNonNull(database, "database should 
not be null");
+            this.table = Objects.requireNonNull(table, "table should not be 
null");
+        }
+
+        public DatabaseIf<?> getDatabase() {
+            return database;
+        }
+
+        public TableIf getTable() {
+            return table;
+        }
+    }
+
+    public static final class GroupCommitTarget implements AutoCloseable {

Review Comment:
   The current head cannot pass the required FE style gate. CheckStyle reports 
`MissingJavadocType` for this new public nested class and also reports the 
unused local `OlapTable table` in 
`GroupCommitPlannerTargetRaceTest.dropFailsWhileCacheMissPlanIsAdmitted()` at 
line 129. Please add the required type Javadoc and remove/use that test 
variable; run 29939849908 contains exactly these two errors.



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