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


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java:
##########
@@ -105,12 +113,49 @@ public IcebergExternalMetaCache(ExecutorService 
refreshExecutor) {
 
     public Table getIcebergTable(ExternalTable dorisTable) {
         NameMapping nameMapping = dorisTable.getOrBuildNameMapping();
-        return 
tableEntry.get(nameMapping.getCtlId()).get(nameMapping).getIcebergTable();
+        IcebergTableCacheValue.Lease lease = statementLease(nameMapping);
+        if (lease != null) {
+            return lease.getIcebergTable();
+        }
+        // Background/bootstrap callers without a StatementContext have no 
deterministic release boundary.
+        // Load directly instead of borrowing a cache generation that could be 
evicted while they use it.
+        return loadTable(nameMapping);

Review Comment:
   [P1] Bound no-context Iceberg table loads
   
   When no `StatementContext` exists, this returns a raw `loadTable()` result 
without a native/HMS load guard, lease, or `tableCleanup()`. With partition 
analysis enabled, `StatisticsAutoCollector` reaches this path via 
`isPartitionedTable()`, so repeated probes can leak table-owned FileIO for 
Glue/S3 Tables/qualifying REST tables and race runtime retirement. Use a 
bounded `withIcebergTable()` projection or another exact-generation operation 
owner.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -782,7 +785,7 @@ public TableScan createTableScan() throws UserException {
             this.pushdownIcebergPredicates.add(predicate.toString());
         }
 
-        icebergTableScan = 
scan.planWith(source.getCatalog().getThreadPoolWithPreAuth());
+        icebergTableScan = scan.planWith(planningExecutor);

Review Comment:
   [P1] Retain the Iceberg generation for the async planner
   
   `doStartSplit()` launches this planning work on an unrecorded future, but 
the exact Table/FileIO/catalog/executor lease remains owned only by 
`StatementContext`. On cancellation or timeout the statement can close while 
the task remains blocked in `planFiles()`, after which refresh/reset may retire 
G1 underneath it. Give the future an independent exact-generation owner 
released from its actual-terminal callback and register a cancellation handle 
with the split assignment.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -267,6 +269,7 @@ protected void doInitialize() throws UserException {
             getRelationSnapshot();
             icebergTable = source.getIcebergTable();
             icebergTable = useFrozenTableGeneration(icebergTable);
+            planningExecutor = getPlanningExecutor();

Review Comment:
   [P1] Keep credentials with the retained Iceberg generation
   
   This selects the exact executor from the retained G1 table, but 
`doInitialize()` immediately reads the authenticator and both storage-property 
maps from the mutable live catalog. ALTER/reset can therefore assemble a G1 
table/executor with G2 credentials/properties (or a temporarily null 
authenticator), and retained-table sink paths repeat the same lookup. Carry an 
immutable authenticator/property bundle in the same generation lease.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/AbstractStreamingTask.java:
##########
@@ -94,35 +98,76 @@ public long getRunningBackendId() {
     }
 
     public void execute() throws JobException {
-        while (retryCount <= MAX_RETRY) {
-            try {
-                before();
-                run();
-                onSuccess();
-                return;
-            } catch (Exception e) {
+        synchronized (executionCompletion) {
+            executionStarted = true;
+            executionOwner = Thread.currentThread();
+        }
+        try {
+            while (retryCount <= MAX_RETRY) {
+                Exception attemptFailure = null;
+                try {
+                    before();
+                    run();
+                } catch (Exception e) {
+                    attemptFailure = e;
+                } finally {
+                    // Only the scheduler worker that created this attempt's 
ConnectContext may tear it down.
+                    // A cancelling thread waits for this handoff instead of 
racing before() and clearing fields
+                    // while planning is still publishing them.
+                    try {
+                        closeOrReleaseResources();
+                    } catch (RuntimeException cleanupFailure) {
+                        if (attemptFailure == null) {
+                            attemptFailure = cleanupFailure;
+                        } else {
+                            attemptFailure.addSuppressed(cleanupFailure);
+                        }
+                    }
+                }
+                if (attemptFailure == null) {
+                    onSuccess();

Review Comment:
   [P1] Handle failures from the success callback
   
   `onSuccess()` is still outside the attempt-failure block. 
`StreamingInsertTask` publishes `SUCCESS` before job-level successor creation 
can throw, while `scheduleOneTask()` only logs the escaped exception and 
removes the old task; the RUNNING job can then have no successor or failure 
transition. Make success/successor publication explicit and recover without 
retrying an already-committed insert.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java:
##########
@@ -552,59 +628,305 @@ private void initPrunedPartitions() throws UserException 
{
             throw new UserException(ExceptionUtils.getRootCauseMessage(e), e);
         }
         partitionInit = true;
+        ensureHmsRuntimeGeneration();
     }
 
     @Override
     public void startSplit(int numBackends) {
+        ensureHmsRuntimeGeneration();
         if (prunedPartitions.isEmpty()) {
             splitAssignment.finishSchedule();
+            releaseFsViewOnce();
             return;
         }
-        AtomicInteger numFinishedPartitions = new AtomicInteger(0);
+        acquireFsView();
         ExecutorService scheduleExecutor = 
Env.getCurrentEnv().getExtMetaCacheMgr().getScheduleExecutor();
+        Executor producerExecutor = 
Env.getCurrentEnv().getExtMetaCacheMgr().getFileListingExecutor();
         long startTime = System.currentTimeMillis();
-        CompletableFuture.runAsync(() -> {
-            for (HivePartition partition : prunedPartitions) {
-                if (batchException.get() != null || splitAssignment.isStop()) {
-                    break;
-                }
-                try {
-                    splittersOnFlight.acquire();
-                } catch (InterruptedException e) {
-                    batchException.set(new UserException(e.getMessage(), e));
-                    break;
+        BatchFsViewOwner createdOwner = new BatchFsViewOwner(splitAssignment, 
fsViewLease);
+        BatchFsViewOwner batchOwner = createdOwner;
+        ConnectContext connectContext = ConnectContext.get();
+        StatementContext statementContext = connectContext == null ? null : 
connectContext.getStatementContext();
+        if (statementContext != null) {
+            try {
+                batchOwner = statementContext.getOrRegisterStatementResource(
+                        batchFsViewResourceKey, () -> createdOwner);
+                if (batchOwner != createdOwner) {
+                    createdOwner.finish();
+                    throw new IllegalStateException("Hudi batch split owner 
was registered twice");
                 }
-                CompletableFuture.runAsync(() -> {
+            } catch (RuntimeException e) {
+                createdOwner.finish();
+                throw e;
+            }
+        }
+
+        BatchFsViewOwner finalBatchOwner = batchOwner;
+        AtomicInteger pendingTasks = new AtomicInteger(1); // producer 
reference
+        Runnable taskFinished = () -> {
+            if (pendingTasks.decrementAndGet() == 0) {
+                finishBatchSplit(finalBatchOwner, startTime);
+            }
+        };
+        TerminalTask producerTask = terminalTask(() -> {
+            try {
+                ensureHmsRuntimeGeneration();
+                for (HivePartition partition : prunedPartitions) {
+                    if (batchException.get() != null || 
splitAssignment.isStop()) {
+                        break;
+                    }
                     try {
-                        List<Split> allFiles = Lists.newArrayList();
-                        getPartitionSplits(partition, allFiles, false);
-                        if (allFiles.size() > numSplitsPerPartition.get()) {
-                            numSplitsPerPartition.set(allFiles.size());
-                        }
-                        if (splitAssignment.needMoreSplit()) {
-                            splitAssignment.addToQueue(allFiles);
-                        }
-                    } catch (Exception e) {
-                        batchException.set(new UserException(e.getMessage(), 
e));
-                    } finally {
+                        splittersOnFlight.acquire();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        recordBatchException(e);
+                        break;
+                    }
+                    if (batchException.get() != null || 
splitAssignment.isStop()) {
                         splittersOnFlight.release();
-                        if (batchException.get() != null) {
-                            splitAssignment.setException(batchException.get());
-                        }
-                        if (numFinishedPartitions.incrementAndGet() == 
prunedPartitions.size()) {
-                            if (getSummaryProfile() != null) {
-                                
getSummaryProfile().addExternalTableGetFileScanTasksTime(
-                                        System.currentTimeMillis() - 
startTime);
+                        break;
+                    }
+                    pendingTasks.incrementAndGet();
+                    TerminalTask partitionTask = terminalTask(() -> {
+                        try {
+                            ensureHmsRuntimeGeneration();
+                            List<Split> allFiles = Lists.newArrayList();
+                            getPartitionSplits(partition, allFiles, false);
+                            if (allFiles.size() > numSplitsPerPartition.get()) 
{
+                                numSplitsPerPartition.set(allFiles.size());
                             }
-                            splitAssignment.finishSchedule();
+                            if (splitAssignment.needMoreSplit()) {
+                                ensureHmsRuntimeGeneration();
+                                splitAssignment.addToQueue(allFiles);
+                            }
+                        } catch (Throwable t) {
+                            recordBatchException(t);
                         }
+                    }, () -> {
+                        splittersOnFlight.release();
+                        taskFinished.run();
+                    });
+                    finalBatchOwner.track(partitionTask);
+                    try {
+                        scheduleExecutor.execute(partitionTask);
+                    } catch (RuntimeException e) {
+                        recordBatchException(e);
+                        partitionTask.cancelBeforeStart();
+                        break;
                     }
-                }, scheduleExecutor);
+                }
+            } catch (Throwable t) {
+                recordBatchException(t);
+            }
+        }, taskFinished);
+        finalBatchOwner.track(producerTask);
+        try {
+            producerExecutor.execute(producerTask);
+        } catch (RuntimeException e) {
+            recordBatchException(e);
+            producerTask.cancelBeforeStart();
+        }
+    }
+
+    private TerminalTask terminalTask(Runnable task, Runnable taskFinished) {
+        return new TerminalTask(task, taskFinished);
+    }
+
+    @VisibleForTesting
+    static class TerminalTask extends FutureTask<Void> {
+        private final AtomicBoolean started = new AtomicBoolean();
+        private final AtomicBoolean interruptRequested = new AtomicBoolean();
+        private final Runnable taskFinished;
+        private volatile Thread runner;
+        private volatile Runnable ownerDone = () -> { };
+
+        TerminalTask(Runnable task, Runnable taskFinished) {
+            super(task, null);
+            this.taskFinished = taskFinished;
+        }
+
+        @Override
+        public void run() {
+            if (started.compareAndSet(false, true)) {
+                runner = Thread.currentThread();
+                if (interruptRequested.get()) {
+                    runner.interrupt();
+                }
+                try {
+                    super.run();
+                } finally {
+                    runner = null;
+                }
             }
+        }
+
+        boolean cancelBeforeStart() {
+            return started.compareAndSet(false, true) && cancel(false);
+        }
+
+        void requestStop() {
+            if (cancelBeforeStart()) {
+                return;
+            }
+            interruptRequested.set(true);
+            Thread runningThread = runner;
+            if (runningThread != null) {
+                runningThread.interrupt();
+            }
+        }
+
+        void setOwnerDone(Runnable ownerDone) {
+            this.ownerDone = ownerDone;
+        }
+
+        @Override
+        protected void done() {
+            try {
+                taskFinished.run();
+            } finally {
+                ownerDone.run();
+            }
+        }
+    }
+
+    private void recordBatchException(Throwable t) {
+        batchException.compareAndSet(null, new UserException(t.getMessage(), 
t));

Review Comment:
   [P1] Publish Hudi batch errors immediately
   
   This only stores the first batch exception; `SplitAssignment.setException()` 
now runs from `finishBatchSplit()`, gated on every accepted sibling becoming 
terminal. If one partition fails while another filesystem call remains blocked, 
initial planning/fetchers see timeouts instead of the known error. Publish the 
first error to `SplitAssignment` immediately while keeping independent 
last-task accounting for exact lease release.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java:
##########
@@ -556,19 +556,33 @@ public void alterJob(AlterJobCommand alterJobCommand) 
throws AnalysisException,
 
     @Override
     public void updateJobStatus(JobStatus status) throws JobException {
+        AbstractStreamingTask taskToCancel = null;
+        boolean waitForTask = JobStatus.PAUSED.equals(status);
         lock.writeLock().lock();
         try {
-            super.updateJobStatus(status);
-            if (JobStatus.PAUSED.equals(getJobStatus())) {
-                clearRunningStreamTask(status);
+            if ((JobStatus.PAUSED.equals(status) || 
JobStatus.STOPPED.equals(status))
+                    && status != getJobStatus()) {
+                taskToCancel = runningStreamTask;

Review Comment:
   [P1] Preserve the manual CDC reader handoff
   
   The manual command calls `onManualStatusAltered()` only after this status 
transition, but the transition has already removed the task. The hook therefore 
neither sets `needRebuildReader` nor calls `releaseRemoteReader()`, while 
`StreamingMultiTblTask.cancel()` intentionally leaves both actions to that 
hook. Carry the captured task into the manual handoff or perform the 
rebuild/release marking before discarding it.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/AbstractStreamingTask.java:
##########
@@ -94,35 +98,76 @@ public long getRunningBackendId() {
     }
 
     public void execute() throws JobException {
-        while (retryCount <= MAX_RETRY) {
-            try {
-                before();
-                run();
-                onSuccess();
-                return;
-            } catch (Exception e) {
+        synchronized (executionCompletion) {
+            executionStarted = true;
+            executionOwner = Thread.currentThread();
+        }
+        try {
+            while (retryCount <= MAX_RETRY) {
+                Exception attemptFailure = null;
+                try {
+                    before();
+                    run();
+                } catch (Exception e) {
+                    attemptFailure = e;
+                } finally {
+                    // Only the scheduler worker that created this attempt's 
ConnectContext may tear it down.
+                    // A cancelling thread waits for this handoff instead of 
racing before() and clearing fields
+                    // while planning is still publishing them.
+                    try {
+                        closeOrReleaseResources();
+                    } catch (RuntimeException cleanupFailure) {

Review Comment:
   [P1] Do not retry a completed insert for cleanup failure
   
   This turns a standalone cleanup exception into an ordinary attempt failure. 
For `StreamingInsertTask`, `run()` has already completed the insert and 
production leaves `noRetry` false, so a post-commit unregister or 
statement-close failure re-enters `before()`/`run()` instead of calling 
`onSuccess()`. Even if the reused label prevents duplicate rows, the committed 
batch can be reported failed/paused and its transaction callback lock spans the 
retry. Track execution and cleanup outcomes separately; cleanup-only failure 
must not replay a completed insert.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java:
##########
@@ -556,19 +556,33 @@ public void alterJob(AlterJobCommand alterJobCommand) 
throws AnalysisException,
 
     @Override
     public void updateJobStatus(JobStatus status) throws JobException {
+        AbstractStreamingTask taskToCancel = null;
+        boolean waitForTask = JobStatus.PAUSED.equals(status);
         lock.writeLock().lock();
         try {
-            super.updateJobStatus(status);
-            if (JobStatus.PAUSED.equals(getJobStatus())) {
-                clearRunningStreamTask(status);
+            if ((JobStatus.PAUSED.equals(status) || 
JobStatus.STOPPED.equals(status))
+                    && status != getJobStatus()) {
+                taskToCancel = runningStreamTask;
+                runningStreamTask = null;
             }
+            super.updateJobStatus(status);

Review Comment:
   [P2] Preserve canceled-task accounting
   
   At this point `runningStreamTask` has already been nulled, so 
`super.updateJobStatus()` dynamically calls `cancelAllTasks()` and the override 
returns before its active-state check and `canceledTaskCount` increment. The 
later direct cancellation does not update the counter, making active PAUSE/STOP 
transitions disappear from job statistics. Snapshot the captured task's active 
state and increment exactly once.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java:
##########
@@ -556,19 +556,33 @@ public void alterJob(AlterJobCommand alterJobCommand) 
throws AnalysisException,
 
     @Override
     public void updateJobStatus(JobStatus status) throws JobException {
+        AbstractStreamingTask taskToCancel = null;
+        boolean waitForTask = JobStatus.PAUSED.equals(status);
         lock.writeLock().lock();
         try {
-            super.updateJobStatus(status);
-            if (JobStatus.PAUSED.equals(getJobStatus())) {
-                clearRunningStreamTask(status);
+            if ((JobStatus.PAUSED.equals(status) || 
JobStatus.STOPPED.equals(status))
+                    && status != getJobStatus()) {
+                taskToCancel = runningStreamTask;
+                runningStreamTask = null;

Review Comment:
   [P1] Preserve the task through transaction callbacks
   
   Clearing `runningStreamTask` here leaves a still-running insert with no task 
identity for transaction callbacks. `beforeCommitted()` later takes the same 
lock and unconditionally dereferences this field for cancellation, task ID, 
offset, and backend data; it remains null even after the outside cancellation 
returns. Keep a stable captured task through callback terminality or add a 
job-status/null-safe transaction owner.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertTask.java:
##########
@@ -173,24 +174,57 @@ protected void onFail(String errMsg) throws JobException {
     @Override
     public void cancel(boolean needWaitCancelComplete) {
         super.cancel(needWaitCancelComplete);
-        if (null != stmtExecutor) {
+        StmtExecutor executor = stmtExecutor;
+        if (null != executor) {
             log.info("cancelling streaming insert task, job id is {}, task id 
is {}",
                     getJobId(), getTaskId());
-            stmtExecutor.cancel(new Status(TStatusCode.CANCELLED, "streaming 
insert task cancelled"),
+            executor.cancel(new Status(TStatusCode.CANCELLED, "streaming 
insert task cancelled"),
                     needWaitCancelComplete);
         }
+        if (needWaitCancelComplete) {
+            awaitExecutionCompletion();

Review Comment:
   [P1] Make pre-executor cancellation bounded
   
   `before()` can block in remote S3 listing or plan initialization before 
`stmtExecutor` is assigned. In that interval `cancel(true)` has nothing to 
signal and waits without timeout or owner interruption for `executionFinished`, 
so PAUSE can hang indefinitely. Publish an interruptible owner earlier or make 
this wait bounded/asynchronous, and cover the pre-publication barrier in a test.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/hive/source/HiveScanNode.java:
##########
@@ -142,6 +145,13 @@ protected void doInitialize() throws UserException {
             Env.getCurrentHiveTransactionMgr().register(hiveTransaction);
             skipCheckingAcidVersionFile = 
sessionVariable.skipCheckingAcidVersionFile;
         }
+        ensureHmsRuntimeGeneration();

Review Comment:
   [P1] Clean up Hive transactions when the fence aborts planning
   
   The Hive transaction is begun and registered before this new generation 
check. If the check throws, Nereids planning returns an error before 
`handleQueryWithRetry()` or QeProcessor registration, and ordinary MySQL 
cleanup only closes `StatementContext`, leaving the local map entry and remote 
read transaction open. Register the transaction with a planning/statement owner 
immediately or deregister every post-registration initialization failure, using 
the exact runtime needed for cleanup.



##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java:
##########
@@ -556,19 +556,33 @@ public void alterJob(AlterJobCommand alterJobCommand) 
throws AnalysisException,
 
     @Override
     public void updateJobStatus(JobStatus status) throws JobException {
+        AbstractStreamingTask taskToCancel = null;
+        boolean waitForTask = JobStatus.PAUSED.equals(status);
         lock.writeLock().lock();
         try {
-            super.updateJobStatus(status);
-            if (JobStatus.PAUSED.equals(getJobStatus())) {
-                clearRunningStreamTask(status);
+            if ((JobStatus.PAUSED.equals(status) || 
JobStatus.STOPPED.equals(status))
+                    && status != getJobStatus()) {
+                taskToCancel = runningStreamTask;
+                runningStreamTask = null;
             }
+            super.updateJobStatus(status);
             if (isFinalStatus()) {
                 
Env.getCurrentGlobalTransactionMgr().getCallbackFactory().removeCallback(getJobId());
             }
             log.info("Streaming insert job {} update status to {}", 
getJobId(), getJobStatus());
+        } catch (RuntimeException | JobException e) {
+            if (taskToCancel != null) {
+                runningStreamTask = taskToCancel;
+            }
+            throw e;
         } finally {
             lock.writeLock().unlock();
         }
+        if (taskToCancel != null) {
+            // The task owner can need this job's write lock while finishing 
transaction callbacks.
+            // Cancel and wait only after publishing the status and releasing 
the job lock.
+            taskToCancel.cancel(waitForTask);

Review Comment:
   [P1] Make PAUSE and its reason scheduler-atomic
   
   The method unlocks with `PAUSED` visible and can then wait in 
`cancel(true)`, while manual alteration installs `MANUAL_PAUSE_ERR` only after 
this method returns. A scheduler tick can observe the old resumable reason, 
call `autoResumeHandler()`, and move the job to `PENDING`; the later hook 
changes only the reason and never restores `PAUSED`. Gate auto-resume through 
transition completion or publish reason/status atomically; `fetchMeta()` needs 
the same ordering fix.



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