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 a8476f988cf Stop a lingering receiving mailbox from pinning the 
receive operator (#19465)
a8476f988cf is described below

commit a8476f988cfd823b36e0069f6d00f3bfd391a531
Author: Yash Mayya <[email protected]>
AuthorDate: Mon Sep 7 06:47:25 2026 -0400

    Stop a lingering receiving mailbox from pinning the receive operator 
(#19465)
---
 .../operator/BaseMailboxReceiveOperator.java       | 21 +++++++----
 .../operator/MailboxReceiveOperatorTest.java       | 41 ++++++++++++++++++++++
 2 files changed, 56 insertions(+), 6 deletions(-)

diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/BaseMailboxReceiveOperator.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/BaseMailboxReceiveOperator.java
index 4e19b8a3a47..da5afb98b9e 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/BaseMailboxReceiveOperator.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/BaseMailboxReceiveOperator.java
@@ -73,7 +73,7 @@ public abstract class BaseMailboxReceiveOperator extends 
MultiStageOperator {
       for (String mailboxId : _mailboxIds) {
         ReceivingMailbox receivingMailbox = 
_mailboxService.getReceivingMailbox(mailboxId);
         
receivingMailbox.registerReceiveOperatorThreadContext(QueryThreadContext.getIfAvailable());
-        ReadMailboxAsyncStream asyncStream = new 
ReadMailboxAsyncStream(receivingMailbox, this);
+        ReadMailboxAsyncStream asyncStream = new 
ReadMailboxAsyncStream(receivingMailbox, _mailboxService);
         asyncStreams.add(asyncStream);
         _receivingStats.add(asyncStream._mailbox.getStatMap());
       }
@@ -164,13 +164,20 @@ public abstract class BaseMailboxReceiveOperator extends 
MultiStageOperator {
     _statMap.merge(StatKey.UPSTREAM_WAIT_MS, 
from.getLong(ReceivingMailbox.StatKey.WAIT_CPU_TIME_MS));
   }
 
+  /// Adapts a [ReceivingMailbox] to the [AsyncStream] the 
[BlockingMultiStreamConsumer] reads from.
+  ///
+  /// Deliberately holds the [MailboxService] and not the operator. The 
mailbox keeps this stream reachable through
+  /// the reader callback registered on it, and after a failed query the 
mailbox itself stays in the service's cache
+  /// until it expires (see [#poll()] for why it is not released earlier). A 
reference back to the operator from here
+  /// would keep the operator, its [OpChainExecutionContext] and everything 
reachable from them alive for that whole
+  /// time.
   private static class ReadMailboxAsyncStream implements 
AsyncStream<ReceivingMailbox.MseBlockWithStats> {
     final ReceivingMailbox _mailbox;
-    final BaseMailboxReceiveOperator _operator;
+    final MailboxService _mailboxService;
 
-    ReadMailboxAsyncStream(ReceivingMailbox mailbox, 
BaseMailboxReceiveOperator operator) {
+    ReadMailboxAsyncStream(ReceivingMailbox mailbox, MailboxService 
mailboxService) {
       _mailbox = mailbox;
-      _operator = operator;
+      _mailboxService = mailboxService;
     }
 
     @Override
@@ -186,9 +193,11 @@ public abstract class BaseMailboxReceiveOperator extends 
MultiStageOperator {
       if (blockWithStats != null) {
         MseBlock block = blockWithStats.getBlock();
 
-        // TODO: Check if we should also release mailbox on not successful EOS.
+        // Only a successful EOS releases the mailbox from the service's 
cache. After an error the senders may still
+        // be running, and they have to find the cancelled mailbox rather than 
recreate a fresh one that nobody reads.
+        // The cache expiry takes care of it, and this stream holds nothing 
that makes that wait expensive.
         if (block.isSuccess()) {
-          _operator._mailboxService.releaseReceivingMailbox(_mailbox);
+          _mailboxService.releaseReceivingMailbox(_mailbox);
         }
       }
       return blockWithStats;
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MailboxReceiveOperatorTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MailboxReceiveOperatorTest.java
index 0af5a807b3d..b2efc3652df 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MailboxReceiveOperatorTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MailboxReceiveOperatorTest.java
@@ -19,6 +19,8 @@
 package org.apache.pinot.query.runtime.operator;
 
 import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -244,6 +246,45 @@ public class MailboxReceiveOperatorTest {
     }
   }
 
+  /// After an error the mailbox stays in the service's cache until it 
expires, and the reader callback it holds keeps
+  /// the stream that adapts it reachable. That stream must not point back at 
the operator, or every failed query
+  /// would pin its operator tree and execution context for as long as the 
mailbox lingers.
+  @Test
+  public void shouldNotStayReachableFromTheMailboxAfterAnError()
+      throws InterruptedException {
+    ReceivingMailbox mailbox = new ReceivingMailbox(MAILBOX_ID_1);
+    
when(_mailboxService.getReceivingMailbox(eq(MAILBOX_ID_1))).thenReturn(mailbox);
+    MailboxReceiveOperator operator = getOperator(_stageMetadata1, 
RelDistribution.Type.SINGLETON);
+    mailbox.offer(ErrorMseBlock.fromException(new RuntimeException("TEST 
ERROR")), List.of(), 1000L);
+    assertTrue(operator.nextBlock().isError());
+    operator.cancel(new RuntimeException("TEST ERROR"));
+    operator.close();
+
+    ReferenceQueue<MailboxReceiveOperator> queue = new ReferenceQueue<>();
+    WeakReference<MailboxReceiveOperator> operatorRef = new 
WeakReference<>(operator, queue);
+    operator = null;
+    assertTrue(awaitCleared(operatorRef, queue), "the mailbox must not keep 
the closed operator reachable");
+    // The mailbox is the retention path under test, so it has to stay 
strongly reachable until this point.
+    assertEquals(mailbox.getId(), MAILBOX_ID_1);
+  }
+
+  /// Waits for `ref` to be cleared, prompting collection as it goes. The 
reference queue does the waiting rather than a
+  /// sleep loop over [WeakReference#get()], so the expected case costs one 
collection instead of a fixed delay, and the
+  /// failing case still gives the collector several attempts before giving up.
+  private static boolean awaitCleared(WeakReference<?> ref, ReferenceQueue<?> 
queue)
+      throws InterruptedException {
+    long deadlineMs = System.currentTimeMillis() + 5_000L;
+    do {
+      System.gc();
+      if (queue.remove(200L) != null) {
+        return true;
+      }
+    } while (System.currentTimeMillis() < deadlineMs);
+    // The collector clears the reference before enqueueing it, so check 
directly rather than call a late enqueue a
+    // failure.
+    return ref.get() == null;
+  }
+
   @Test
   public void shouldEarlyTerminateMailboxesWhenIndicated() {
     Object[] row1 = new Object[]{1, 1};


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

Reply via email to