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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java:
##########
@@ -73,6 +73,9 @@ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
     @Override
     public void run(ConnectContext ctx, StmtExecutor executor) throws 
Exception {
         StatementContext statementContext = ctx.getStatementContext();
+        // PREPARE retains this StatementContext, but ConnectProcessor closes 
the resources from each
+        // COM_STMT_EXECUTE. Reopen an empty generation before the next 
execution starts planning.
+        statementContext.beginStatementResourceGeneration();

Review Comment:
   [P1] End the forwarded PREPARE generation before EXECUTE
   
   `prepareExecuteBuffer` runs analyzed PREPARE and immediately calls 
`handleExecute()` without closing resources between the two phases. Binding an 
Iceberg relation during PREPARE registers a table lease, so this call sees a 
nonempty map and throws `Previous statement resources are still active` before 
the first forwarded execution. Please close the PREPARE resource generation 
before `handleExecute()` and let EXECUTE open a fresh one, with coverage for a 
forwarded prepared Iceberg query.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java:
##########
@@ -558,53 +594,213 @@ private void initPrunedPartitions() throws UserException 
{
     public void startSplit(int numBackends) {
         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 {
+                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 {
+                            List<Split> allFiles = Lists.newArrayList();
+                            getPartitionSplits(partition, allFiles, false);
+                            if (allFiles.size() > numSplitsPerPartition.get()) 
{
+                                numSplitsPerPartition.set(allFiles.size());
+                            }
+                            if (splitAssignment.needMoreSplit()) {
+                                splitAssignment.addToQueue(allFiles);
                             }
-                            splitAssignment.finishSchedule();
+                        } 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 Runnable taskFinished;
+
+        TerminalTask(Runnable task, Runnable taskFinished) {
+            super(task, null);
+            this.taskFinished = taskFinished;
+        }
+
+        @Override
+        public void run() {
+            if (started.compareAndSet(false, true)) {
+                super.run();
             }
+        }
+
+        boolean cancelBeforeStart() {
+            return started.compareAndSet(false, true) && cancel(false);
+        }
+
+        @Override
+        protected void done() {
+            taskFinished.run();
+        }
+    }
+
+    private void recordBatchException(Throwable t) {
+        batchException.compareAndSet(null, new UserException(t.getMessage(), 
t));
+    }
+
+    private void finishBatchSplit(BatchFsViewOwner batchOwner, long startTime) 
{
+        try {
             if (batchException.get() != null) {
                 splitAssignment.setException(batchException.get());
             }
-        }, scheduleExecutor);
+            if (getSummaryProfile() != null) {
+                
getSummaryProfile().addExternalTableGetFileScanTasksTime(System.currentTimeMillis()
 - startTime);
+            }
+            splitAssignment.finishSchedule();
+        } finally {
+            batchOwner.finish();
+        }
+    }
+
+    @VisibleForTesting
+    static class BatchFsViewOwner implements Closeable {
+        private final SplitAssignment splitAssignment;
+        private final HudiFsViewCacheValue.Lease lease;
+        private final AtomicBoolean finished = new AtomicBoolean();
+        private final AtomicReference<RuntimeException> finishFailure = new 
AtomicReference<>();
+        private final CountDownLatch terminal = new CountDownLatch(1);
+        private final ConcurrentLinkedQueue<TerminalTask> tasks = new 
ConcurrentLinkedQueue<>();
+        private final AtomicBoolean stopping = new AtomicBoolean();
+
+        BatchFsViewOwner(SplitAssignment splitAssignment, 
HudiFsViewCacheValue.Lease lease) {
+            this.splitAssignment = splitAssignment;
+            this.lease = lease;
+        }
+
+        void finish() {
+            if (finished.compareAndSet(false, true)) {
+                try {
+                    lease.close();
+                } catch (RuntimeException e) {
+                    finishFailure.set(e);
+                    throw e;
+                } finally {
+                    terminal.countDown();
+                }
+            }
+        }
+
+        void track(TerminalTask task) {
+            tasks.add(task);
+            if (stopping.get()) {
+                task.cancelBeforeStart();
+            }
+        }
+
+        @Override
+        public void close() {
+            RuntimeException stopFailure = null;
+            if (!finished.get()) {
+                stopping.set(true);
+                try {
+                    splitAssignment.stop();
+                } catch (RuntimeException e) {
+                    stopFailure = e;
+                }
+                tasks.forEach(TerminalTask::cancelBeforeStart);
+            }
+            boolean interrupted = false;
+            while (true) {
+                try {
+                    terminal.await();

Review Comment:
   [P1] Do not wait uninterruptibly for stalled listing tasks during cleanup
   
   Once a `TerminalTask` has passed `started`, `cancelBeforeStart()` cannot 
signal it. This close path stops only the split assignment and then ignores 
interrupts while waiting for every running remote listing to finish. If one 
filesystem call stalls, statement cancellation (including Arrow DoGet 
finalization) never returns. Please request cooperative 
cancellation/interruption of running tasks and move exact lease release to 
their actual-terminal callback, so cleanup can return without releasing the 
fs-view early. A test with an already-started blocked listing should prove 
cleanup returns while the lease stays pinned until that task exits.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java:
##########
@@ -105,12 +112,46 @@ 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);
+    }
+
+    /** Returns the executor owned by the exact table generation retained by 
this statement. */
+    ThreadPoolExecutor getIcebergTableExecutor(ExternalTable dorisTable) {
+        NameMapping nameMapping = dorisTable.getOrBuildNameMapping();
+        IcebergTableCacheValue.Lease lease = statementLease(nameMapping);
+        if (lease == null || lease.getPlanningExecutor() == null) {
+            return dorisTable.getCatalog().getThreadPoolWithPreAuth();
+        }
+        return lease.getPlanningExecutor();
+    }
+
+    /** Runs a bounded metadata operation while retaining the exact table 
generation it uses. */
+    <T> T withIcebergTable(ExternalTable dorisTable, Function<Table, T> 
action) {
+        NameMapping nameMapping = dorisTable.getOrBuildNameMapping();
+        IcebergTableCacheValue.Lease statementLease = 
statementLease(nameMapping);
+        if (statementLease != null) {
+            return action.apply(statementLease.getIcebergTable());
+        }
+        try (IcebergTableCacheValue.Lease operationLease = 
borrow(nameMapping)) {
+            return action.apply(operationLease.getIcebergTable());
+        }
     }
 
     public IcebergSnapshotCacheValue getSnapshotCache(ExternalTable 
dorisTable) {
         NameMapping nameMapping = dorisTable.getOrBuildNameMapping();
-        return 
tableEntry.get(nameMapping.getCtlId()).get(nameMapping).getLatestSnapshotCacheValue();
+        IcebergTableCacheValue.Lease lease = statementLease(nameMapping);

Review Comment:
   [P1] Give MV-cache planning leases an owner
   
   This registration also runs while `MTMVCache.from()` plans an Iceberg-backed 
MV definition with a fresh internal `StatementContext`. Its finally restores 
flags and the caller's thread-local context but never closes or transfers that 
statement's leases, and the fresh-context callers have no later owner. Repeated 
cache generation therefore pins table/FileIO/catalog/executor generations; 
simply closing at the end of planning may also be too early if the returned 
cached plan retains the frozen table. Please either transfer the leases to the 
`MTMVCache` lifetime or remove runtime-bearing snapshot state before returning 
it, with success/failure and cache-replacement coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/MetaCacheEntry.java:
##########
@@ -231,6 +257,7 @@ private V getWithManualLoad(K key, Function<K, V> 
loadFunction) {
             long generation = invalidateGeneration.get();

Review Comment:
   [P1] Seal entries when their catalog group is removed
   
   The generation checks here are local to this `MetaCacheEntry`, so they do 
not cover removal of the containing catalog group. A caller can obtain old 
entry E, pause, let `invalidateCatalog()` remove E's group and finish its one 
`invalidateAll()`, then resume here: it snapshots the already-incremented 
generation, loads, and publishes into E successfully. Future catalog 
invalidations address a replacement group, so the Hudi/Iceberg retirement 
callback for this detached value is no longer reachable through catalog 
lifecycle and can pin its filesystem/catalog generation. Please seal removed 
groups/entries (retiring any post-seal load and forcing the caller to 
re-resolve the current group), with a race test that starts the old-entry load 
only after removal completes.



##########
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] Carry the frozen snapshot's executor into child scans
   
   An installed snapshot can come from catalog generation G1, but this child 
first borrows current G2, replaces only its table with the frozen G1 table, and 
then resolves `planningExecutor` from the G2 lease. A reset between parent 
snapshot capture and child initialization therefore runs G1 
`TableOperations`/FileIO work under G2's pre-auth executor. Please transfer the 
exact table lease/executor with `IcebergMvccSnapshot` (or otherwise choose both 
atomically from that snapshot), and cover reset-between-capture-and-child-plan 
for rewrite and MTMV contexts.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergExternalMetaCache.java:
##########
@@ -159,23 +200,132 @@ public void invalidateCatalogEntries(long catalogId) {
     }
 
     private IcebergTableCacheValue loadTableCacheValue(NameMapping 
nameMapping) {
+        CatalogIf catalog = 
Env.getCurrentEnv().getCatalogMgr().getCatalog(nameMapping.getCtlId());
+        if (catalog instanceof IcebergExternalCatalog) {
+            IcebergExternalCatalog icebergCatalog = (IcebergExternalCatalog) 
catalog;
+            try (IcebergExternalCatalog.TableLoadContext loadContext = 
icebergCatalog.beginTableLoad()) {
+                IcebergMetadataOps ops = loadContext.getOps();
+                Table table;
+                try {
+                    table = 
loadContext.loadTable(nameMapping.getRemoteDbName(), 
nameMapping.getRemoteTblName());
+                } catch (Exception e) {
+                    throw new 
RuntimeException(ExceptionUtils.getRootCauseMessage(e), e);
+                }
+                ExternalTable dorisTable = findExternalTable(nameMapping, 
ENGINE);
+                Runnable tableCleanup = 
tableCleanup(loadContext.getCatalogType(), ops, table);
+                IcebergCatalogResourceTracker.ResourceLease catalogLease = 
loadContext.promote();
+                return new IcebergTableCacheValue(table, 
ops.getThreadPoolWithPreAuth(),
+                        () -> loadSnapshotProjection(dorisTable, table), () -> 
{
+                    try {
+                        tableCleanup.run();
+                    } finally {
+                        catalogLease.close();
+                    }
+                });
+            }
+        }
+        Table table = loadTable(nameMapping);
+        IcebergMetadataOps ops = resolveMetadataOps(catalog);
+        ExternalTable dorisTable = findExternalTable(nameMapping, ENGINE);
+        return new IcebergTableCacheValue(table, () -> 
loadSnapshotProjection(dorisTable, table),

Review Comment:
   [P1] Fence HMS Iceberg table generations across runtime reset
   
   This fallback is used by Iceberg-format `HMSExternalTable`s, but it misses 
both sides of the runtime-generation fence. First, an active statement can 
retain the G1 table while reset immediately closes G1's `icebergMetadataOps` 
and pre-auth pool; the scan then plans that table on a shut-down or G2 executor 
because this value stores neither the exact executor nor a runtime owner. 
Second, `HMSExternalCatalog.resetToUninitialized(false)` and its 
property-update hook never remove `IcebergExternalMetaCache.ENGINE`, so even a 
statement starting after reset can keep borrowing the retired G1 value. Please 
remove the HMS Iceberg group before rotation, capture the exact 
executor/runtime with each value, and defer G1 retirement until pre-fence 
borrowers drain. A regression should prove an active G1 statement survives 
while a new post-reset statement reloads G2.



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