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

gortiz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 91416079849 Close op chain when scheduling fails to release operator 
resources (#18928)
91416079849 is described below

commit 91416079849599668de91c200eb16aebc1288f46
Author: Yash Mayya <[email protected]>
AuthorDate: Tue Jul 7 01:33:30 2026 -0700

    Close op chain when scheduling fails to release operator resources (#18928)
---
 .../java/org/apache/pinot/query/runtime/QueryRunner.java   | 14 +++++++++++++-
 .../query/runtime/executor/OpChainSchedulerService.java    | 13 +++++++++----
 2 files changed, 22 insertions(+), 5 deletions(-)

diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/QueryRunner.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/QueryRunner.java
index afc05d45187..910f53480e4 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/QueryRunner.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/QueryRunner.java
@@ -324,8 +324,8 @@ public class QueryRunner {
     OpChainExecutionContext executionContext =
         OpChainExecutionContext.fromQueryContext(_mailboxService, 
opChainMetadata, stageMetadata, workerMetadata,
             pipelineBreakerResult, sendStats, 
_keepPipelineBreakerStats.getAsBoolean());
+    OpChain opChain = null;
     try {
-      OpChain opChain;
       if (workerMetadata.isLeafStageWorker()) {
         Map<String, String> rlsFilters = 
RlsUtils.extractRlsFilters(requestMetadata);
         opChain =
@@ -337,6 +337,18 @@ public class QueryRunner {
       // This can fail if the executor rejects the task.
       _opChainScheduler.register(opChain);
     } catch (RuntimeException e) {
+      // register() can fail after the op chain was built — the query was 
cancelled/terminated before scheduling
+      // (checkTermination / cancelled-query cache), or the executor rejected 
the task (e.g. under load shedding). In
+      // those cases the op chain is never scheduled, so the scheduler's 
FutureCallback, which normally close()s the
+      // chain on completion, never fires — leaking any operator resources 
that are released only in close(). Close the
+      // built chain here (idempotent; guarded so a close failure cannot mask 
the original error).
+      if (opChain != null) {
+        try {
+          opChain.close();
+        } catch (RuntimeException closeError) {
+          LOGGER.warn("Failed to close op chain after scheduling failure", 
closeError);
+        }
+      }
       ErrorMseBlock errorBlock = ErrorMseBlock.fromException(e);
       tryPropagateErrorViaOpChainConverter(workerMetadata, stagePlan, 
opChainMetadata, pipelineBreakerResult,
           errorBlock);
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
index e4472a41629..75de3ad8eda 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java
@@ -254,10 +254,15 @@ public class OpChainSchedulerService {
       _executorService.submit(listenableFutureTask);
     } catch (RuntimeException e) {
       // The MSE executor is wrapped in HardLimitExecutor + heap throttling, 
so submit() can throw
-      // RejectedExecutionException. When it does, the task never runs and the 
directExecutor FutureCallback above
-      // (which decrements the active-opchain counter and removes the 
per-request context entry) never fires. Back
-      // out that bookkeeping here so the entry — and the 
QueryExecutionContext it pins — does not leak until a later
-      // cancel. Then rethrow so the caller propagates the failure as a stage 
error.
+      // RejectedExecutionException. When it does, the task never runs, so 
neither the directExecutor FutureCallback
+      // above (which decrements the active-opchain counter and removes the 
per-request context entry) nor runJob's
+      // _opChainCache.invalidate() ever fires. Back out both here: invalidate 
the cache entry this method put()
+      // above so it does not pin the rootOperator tree and 
QueryExecutionContext until TTL/weight eviction (the same
+      // prompt release cancel() performs for cancelled entries), and back out 
the active-opchain bookkeeping so the
+      // per-request context entry does not leak until a later cancel. Then 
rethrow so the caller propagates the
+      // failure as a stage error. The caller, 
QueryRunner#processQueryBlocking, close()s the op chain on this
+      // rethrow, releasing operator resources that the never-fired 
FutureCallback would otherwise have closed.
+      _opChainCache.invalidate(opChainId);
       decrementActiveOpChains(requestId);
       throw e;
     }


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

Reply via email to