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

yashmayya 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 12e7af1dc18 Skip block copies for spool stages whose workers are all 
remote (#19428)
12e7af1dc18 is described below

commit 12e7af1dc18d50981b82eb1f7b08ecb545c3558c
Author: Yash Mayya <[email protected]>
AuthorDate: Mon Sep 21 14:52:34 2026 -0700

    Skip block copies for spool stages whose workers are all remote (#19428)
---
 .../pinot/query/mailbox/GrpcSendingMailbox.java    |  9 ++-
 .../query/mailbox/InMemorySendingMailbox.java      |  6 ++
 .../apache/pinot/query/mailbox/SendingMailbox.java | 31 ++++++++-
 .../runtime/operator/MailboxSendOperator.java      |  3 +-
 .../runtime/operator/exchange/BlockExchange.java   | 34 ++++++++++
 .../operator/exchange/BroadcastExchange.java       | 32 +++++-----
 .../query/mailbox/GrpcSendingMailboxTest.java      | 12 ++++
 .../query/mailbox/InMemorySendingMailboxTest.java  | 12 ++++
 .../operator/exchange/BlockExchangeTest.java       | 22 +++++++
 .../operator/exchange/BroadcastExchangeTest.java   | 73 +++++++++++++++++++---
 10 files changed, 204 insertions(+), 30 deletions(-)

diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/GrpcSendingMailbox.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/GrpcSendingMailbox.java
index d8483a1e835..90ae69c8d0e 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/GrpcSendingMailbox.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/GrpcSendingMailbox.java
@@ -116,9 +116,12 @@ public class GrpcSendingMailbox implements SendingMailbox {
     return false;
   }
 
-  /// NOTE: [org.apache.pinot.query.runtime.operator.exchange.BlockExchange] 
implementations rely on this method
-  /// serializing the block synchronously on the calling thread: once it 
returns, the block's contents may be handed
-  /// by reference to a local receiver that mutates them.
+  @Override
+  public boolean deliversByReference() {
+    // Blocks are serialized within send(MseBlock.Data), so the receiver never 
reads this instance
+    return false;
+  }
+
   @Override
   public void send(MseBlock.Data data) {
     QueryThreadContext.checkTerminationAndSampleUsage(SEND_SCOPE);
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/InMemorySendingMailbox.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/InMemorySendingMailbox.java
index 5b94e6ea5a7..c9cef3552d3 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/InMemorySendingMailbox.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/InMemorySendingMailbox.java
@@ -62,6 +62,12 @@ public class InMemorySendingMailbox implements 
SendingMailbox {
     return true;
   }
 
+  @Override
+  public boolean deliversByReference() {
+    // Blocks are offered to the receiving mailbox as they are, so the 
receiver reads the same instance
+    return true;
+  }
+
   @Override
   public void send(MseBlock.Data data) {
     QueryThreadContext.checkTerminationAndSampleUsage(SEND_SCOPE);
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/SendingMailbox.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/SendingMailbox.java
index a5b8f9260b1..790837aa83b 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/SendingMailbox.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/SendingMailbox.java
@@ -33,14 +33,41 @@ import org.apache.pinot.segment.spi.memory.DataBuffer;
 ///   - One call to [#cancel(Throwable)] if the sender wants to cancel the 
receiver
 public interface SendingMailbox extends AutoCloseable {
 
-  /// Returns whether the mailbox is sending data to a local receiver, where 
blocks can be directly passed to the
-  /// receiver.
+  /// Returns whether blocks can be passed to this mailbox whole, instead of 
being split into smaller blocks that
+  /// respect the maximum content size of a mailbox message.
+  ///
+  /// This says nothing about whether the receiver ends up with a reference to 
the block: see
+  /// [#deliversByReference()].
   boolean isLocal();
 
+  /// Returns whether a receiver may keep a reference to the blocks sent to 
this mailbox, instead of reading their
+  /// contents within [#send(MseBlock.Data)]. The answer must not change 
during the life of the mailbox, so callers
+  /// can read it once.
+  ///
+  /// A mailbox that returns `true` here also returns `true` from 
[#isLocal()], because a block can only be given by
+  /// reference to a receiver in this process. The reverse does not hold: a 
local mailbox can still read the block
+  /// within [#send(MseBlock.Data)] and pass on something else.
+  ///
+  /// A caller that sends the same block instance to more than one mailbox, 
when anything downstream mutates the
+  /// contents of that block, must:
+  ///
+  /// 1. Give a copy of the block to all the mailboxes that return `true` 
here, except one.
+  /// 2. Give the original block to that remaining mailbox last, once every 
other mailbox has returned from
+  ///    [#send(MseBlock.Data)].
+  ///
+  /// Step 2 matters as much as step 1. A receiver of the original block can 
mutate it as soon as `send` gives it the
+  /// block, which would corrupt the block for a mailbox that is still reading 
it.
+  ///
+  /// See [org.apache.pinot.query.runtime.operator.exchange.BroadcastExchange].
+  boolean deliversByReference();
+
   /// Sends a data block to the receiver. Note that SendingMailbox are 
required to acquire resources lazily in this
   /// call, and they should **not** acquire any resources when they are 
created. This method should throw if there was
   /// an error sending the data, since that would allow
   /// [org.apache.pinot.query.runtime.operator.exchange.BlockExchange] to exit 
early.
+  ///
+  /// Implementations that return `false` from [#deliversByReference()] must 
finish reading the block before this
+  /// method returns, because the caller may then pass the same block to a 
receiver that mutates it.
   void send(MseBlock.Data data);
 
   /// Sends an EOS block to the receiver. Note that SendingMailbox are 
required to acquire resources lazily in this
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MailboxSendOperator.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MailboxSendOperator.java
index 363dc124470..e90d70cee26 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MailboxSendOperator.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MailboxSendOperator.java
@@ -89,7 +89,8 @@ public class MailboxSendOperator extends MultiStageOperator {
   /// 1. One inner exchange is created for each receiver stage, using the 
method mentioned above and keeping the
   ///    distribution type specified in the [MailboxSendNode].
   /// 2. Then, a single outer broadcast exchange is created to fan out the 
data to all the inner exchanges. It copies
-  ///    blocks that carry aggregation intermediate results so that no two 
receiver stages share them.
+  ///    blocks that carry aggregation intermediate results, so that no two 
receiver stages share them. Stages whose
+  ///    workers all read the block within 
[SendingMailbox#send(MseBlock.Data)] do not need a copy.
   ///
   /// @see BlockExchange#asSendingMailbox(String)
   private static BlockExchange getBlockExchange(OpChainExecutionContext ctx, 
MailboxSendNode node,
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchange.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchange.java
index 56c058c324c..4ad02f16843 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchange.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchange.java
@@ -43,6 +43,7 @@ public abstract class BlockExchange implements AutoCloseable {
   private final List<SendingMailbox> _sendingMailboxes;
   private final BlockSplitter _splitter;
   private final Function<List<SendingMailbox>, Integer> _statsIndexChooser;
+  private final boolean _deliversByReference;
 
   protected static final Function<List<SendingMailbox>, Integer> 
RANDOM_INDEX_CHOOSER =
       (mailboxes) -> ThreadLocalRandom.current().nextInt(mailboxes.size());
@@ -86,6 +87,21 @@ public abstract class BlockExchange implements AutoCloseable 
{
     _sendingMailboxes = sendingMailboxes;
     _splitter = splitter;
     _statsIndexChooser = statsIndexChooser;
+    _deliversByReference = anyDeliversByReference(sendingMailboxes);
+  }
+
+  /// Returns whether any of the given mailboxes delivers blocks by reference. 
Mailboxes are fixed when the exchange
+  /// is created, and each of them gives a constant answer, so this is 
computed once.
+  ///
+  /// The answer therefore counts mailboxes that terminate early later on. 
That only makes a caller take a copy it
+  /// did not need. It never makes a caller skip a copy it did need.
+  private static boolean anyDeliversByReference(List<SendingMailbox> 
sendingMailboxes) {
+    for (SendingMailbox sendingMailbox : sendingMailboxes) {
+      if (sendingMailbox.deliversByReference()) {
+        return true;
+      }
+    }
+    return false;
   }
 
   /// API to send a block to the destination mailboxes.
@@ -164,6 +180,12 @@ public abstract class BlockExchange implements 
AutoCloseable {
     }
   }
 
+  /// Sends the block to the destinations, following the distribution strategy 
of this exchange.
+  ///
+  /// Implementations must finish reading the block before this method 
returns. [BlockExchangeSendingMailbox] reports
+  /// that it does not deliver blocks by reference on that basis, so an 
implementation that queued a block for
+  /// another thread would break [SendingMailbox#deliversByReference()] for 
every exchange that decorates it, and
+  /// silently corrupt the aggregation intermediate results that 
[BroadcastExchange] shares between destinations.
   protected abstract void route(List<SendingMailbox> destinations, 
MseBlock.Data block);
 
   @Override
@@ -213,9 +235,21 @@ public abstract class BlockExchange implements 
AutoCloseable {
 
     @Override
     public boolean isLocal() {
+      // Blocks are handed to the decorated exchange whole, and splitting them 
is left to that exchange.
+      // TODO(#19427): the decorated exchange is currently built with 
BlockSplitter#NO_OP, so blocks sent
+      //       through a multi-send node are never split. See 
MailboxSendOperator#getBlockExchange.
       return true;
     }
 
+    @Override
+    public boolean deliversByReference() {
+      // The decorated exchange passes blocks to its own mailboxes, so this 
mailbox delivers by reference only if
+      // any of those does. The question is whether a receiver keeps the block 
alive after route returns, not whether
+      // the decorated exchange shares one block between its own mailboxes: an 
inner HashExchange gives each of its
+      // mailboxes a different block, but the rows of those blocks still hold 
the cells of this one.
+      return _deliversByReference;
+    }
+
     @Override
     public void send(MseBlock.Data data) {
       if (LOGGER.isTraceEnabled()) {
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchange.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchange.java
index 0d902df14bf..11695ab0f13 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchange.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchange.java
@@ -28,14 +28,14 @@ import 
org.apache.pinot.query.runtime.blocks.RowHeapDataBlock;
 
 /// Broadcast blocks to all the destinations.
 ///
-/// This is the only exchange that routes the same block to more than one 
destination, and local (same-JVM) mailboxes
-/// deliver on-heap blocks by reference. Blocks that
+/// This is the only exchange that routes the same block to more than one 
destination, and some mailboxes
+/// [deliver blocks by reference][SendingMailbox#deliversByReference()]. 
Blocks that
 /// [carry aggregation intermediate 
results][RowHeapDataBlock#containsObjectColumns()] cannot be shared this way:
 /// downstream operators mutate those objects in place when they merge them or 
extract final results, so two
-/// receivers on the same server would corrupt them. For such blocks, [#route] 
gives every local destination except
-/// the first its own [copy][RowHeapDataBlock#copyObjectColumns()]. Remote 
destinations only read the block to
-/// serialize it, before any local receiver can mutate it, so they do not need 
copies. Blocks without OBJECT columns
-/// are shared by reference with all the destinations.
+/// receivers of the same block would corrupt them. For such blocks, [#route] 
gives every destination that delivers
+/// by reference, except the first, its own 
[copy][RowHeapDataBlock#copyObjectColumns()]. The other destinations read
+/// the block within [SendingMailbox#send(MseBlock.Data)], before any receiver 
can mutate it, so they do not need
+/// copies. Blocks without OBJECT columns are shared by reference with all the 
destinations.
 ///
 /// This also protects multi-send (spool) nodes, which fan each block out to 
the exchanges of their receiver stages
 /// through this exchange (see [BlockExchange#asSendingMailbox]).
@@ -60,26 +60,26 @@ class BroadcastExchange extends BlockExchange {
       }
       return;
     }
-    // Send a copy to every active local destination except the first one, 
which receives the original block without
-    // copying. Remote destinations serialize the original block on this 
thread, and the copies are also made on this
-    // thread, so all reads of the original block finish before it is handed 
to a local receiver that can start
-    // mutating it.
+    // Send a copy to every active destination that delivers by reference, 
except the first one, which receives the
+    // original block without copying. The other destinations read the 
original block within send, and the copies are
+    // also made on this thread, so all reads of the original block finish 
before it is handed to a receiver that can
+    // start mutating it.
     RowHeapDataBlock rowHeapBlock = block.asRowHeap();
-    SendingMailbox firstLocalDestination = null;
+    SendingMailbox firstByReferenceDestination = null;
     for (SendingMailbox mailbox : destinations) {
       if (mailbox.isEarlyTerminated()) {
         continue;
       }
-      if (!mailbox.isLocal()) {
+      if (!mailbox.deliversByReference()) {
         sendBlock(mailbox, block);
-      } else if (firstLocalDestination == null) {
-        firstLocalDestination = mailbox;
+      } else if (firstByReferenceDestination == null) {
+        firstByReferenceDestination = mailbox;
       } else {
         sendBlock(mailbox, rowHeapBlock.copyObjectColumns());
       }
     }
-    if (firstLocalDestination != null) {
-      sendBlock(firstLocalDestination, block);
+    if (firstByReferenceDestination != null) {
+      sendBlock(firstByReferenceDestination, block);
     }
   }
 }
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/GrpcSendingMailboxTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/GrpcSendingMailboxTest.java
index 57f074352cf..9241f45b754 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/GrpcSendingMailboxTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/GrpcSendingMailboxTest.java
@@ -80,6 +80,18 @@ public class GrpcSendingMailboxTest {
     }
   }
 
+  @Test
+  public void doesNotDeliverBlocksByReference() {
+    GrpcSendingMailbox mailbox =
+        new GrpcSendingMailbox("test-mailbox", 
Mockito.mock(ChannelManager.class), "localhost", 0, Long.MAX_VALUE,
+            new StatMap<>(MailboxSendOperator.StatKey.class), 4 * 1024 * 1024, 
true);
+
+    // Blocks are serialized within send(MseBlock.Data), so senders that share 
one block between mailboxes do not
+    // need to give this one a copy. See BroadcastExchange.
+    Assert.assertFalse(mailbox.deliversByReference());
+    Assert.assertFalse(mailbox.isLocal());
+  }
+
   /// Regression test for the lazy-initialization data race on 
`_contentObserver`. Before the fix, both `sendInternal`
   /// and `cancel` had an unsynchronized `if (_contentObserver == null) { 
_contentObserver = getContentObserver(); }`
   /// pattern. `_contentObserver` is `volatile` so individual reads/writes are 
atomic, but two threads racing through
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/InMemorySendingMailboxTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/InMemorySendingMailboxTest.java
index a2b4541350a..c550d37af5b 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/InMemorySendingMailboxTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/InMemorySendingMailboxTest.java
@@ -49,4 +49,16 @@ public class InMemorySendingMailboxTest {
       Mockito.verifyNoInteractions(mailboxService);
     }
   }
+
+  @Test
+  public void deliversBlocksByReference() {
+    InMemorySendingMailbox mailbox =
+        new InMemorySendingMailbox("test-mailbox", 
Mockito.mock(MailboxService.class), Long.MAX_VALUE,
+            new StatMap<>(MailboxSendOperator.StatKey.class));
+
+    // Blocks are offered to the receiving mailbox as they are, so senders 
that share one block between mailboxes
+    // must give this one a copy. See BroadcastExchange.
+    Assert.assertTrue(mailbox.deliversByReference());
+    Assert.assertTrue(mailbox.isLocal());
+  }
 }
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchangeTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchangeTest.java
index c2087ecfdbf..907766e21e3 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchangeTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BlockExchangeTest.java
@@ -165,6 +165,28 @@ public class BlockExchangeTest {
     Assert.assertEquals(sentBlocks.get(1).asRowHeap().getRows(), 
outBlockTwo.getRows());
   }
 
+  @Test
+  public void shouldDeliverByReferenceWhenAnyDestinationDoes() {
+    // Given: an exchange whose destinations are one mailbox that serializes 
blocks and one that does not
+    when(_mailbox2.deliversByReference()).thenReturn(true);
+    BlockExchange exchange = new TestBlockExchange(List.of(_mailbox1, 
_mailbox2));
+
+    // Then: the exchange exposed as a mailbox reports that it delivers by 
reference
+    Assert.assertTrue(exchange.asSendingMailbox("1").deliversByReference());
+  }
+
+  @Test
+  public void shouldNotDeliverByReferenceWhenNoDestinationDoes() {
+    // Given: an exchange whose destinations all serialize the blocks they are 
sent
+    BlockExchange exchange = new TestBlockExchange(List.of(_mailbox1, 
_mailbox2));
+
+    // Then: the exchange exposed as a mailbox reports that it does not 
deliver by reference. It still takes blocks
+    // whole, because splitting them is left to the exchange it decorates
+    SendingMailbox sendingMailbox = exchange.asSendingMailbox("1");
+    Assert.assertFalse(sendingMailbox.deliversByReference());
+    Assert.assertTrue(sendingMailbox.isLocal());
+  }
+
   private static class TestBlockExchange extends BlockExchange {
     protected TestBlockExchange(List<SendingMailbox> destinations) {
       this(destinations, (block, size) -> Iterators.singletonIterator(block));
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchangeTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchangeTest.java
index 7255149df72..c370d5a52e4 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchangeTest.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/exchange/BroadcastExchangeTest.java
@@ -63,8 +63,11 @@ public class BroadcastExchangeTest {
   @BeforeMethod
   public void setUp() {
     _mocks = MockitoAnnotations.openMocks(this);
+    // In-memory mailboxes take blocks whole and hand them to the receiver by 
reference
     Mockito.when(_mailbox1.isLocal()).thenReturn(true);
     Mockito.when(_mailbox2.isLocal()).thenReturn(true);
+    Mockito.when(_mailbox1.deliversByReference()).thenReturn(true);
+    Mockito.when(_mailbox2.deliversByReference()).thenReturn(true);
     _aggFunction = mockFunnelAggFunction();
   }
 
@@ -89,7 +92,7 @@ public class BroadcastExchangeTest {
   // getAggFunctions() is deprecated, but the copy must preserve it for 
downstream serialization, so assert on it
   @SuppressWarnings("deprecation")
   @Test
-  public void shouldCopyBlocksWithObjectColumnsForAllLocalDestinationsButOne() 
{
+  public void 
shouldCopyBlocksWithObjectColumnsForAllByReferenceDestinationsButOne() {
     // Given:
     PriorityQueue<FunnelStepEvent> stepEvents = stepEvents(new 
FunnelStepEvent(1000L, 0),
         new FunnelStepEvent(2000L, 1));
@@ -124,16 +127,16 @@ public class BroadcastExchangeTest {
   }
 
   @Test
-  public void shouldSendOriginalBlockToRemoteDestinationsBeforeTheLocalOne() {
-    // Given: a remote destination between two local ones
-    SendingMailbox remoteMailbox = Mockito.mock(SendingMailbox.class);
+  public void 
shouldSendOriginalBlockToOtherDestinationsBeforeTheByReferenceOne() {
+    // Given: a destination that reads the block within send, between two that 
deliver it by reference
+    SendingMailbox remoteMailbox = remoteMailbox();
     RowHeapDataBlock block = funnelBlock(stepEvents(new FunnelStepEvent(1000L, 
0)));
 
     // When:
     route(block, remoteMailbox, _mailbox1, _mailbox2);
 
-    // Then: the remote destination serializes the original, the first local 
destination receives the original by
-    // reference after all other reads of it, and the extra local destination 
receives a copy
+    // Then: the remote destination serializes the original, the first 
by-reference destination receives the original
+    // after all other reads of it, and the extra by-reference destination 
receives a copy
     assertSame(capturedBlock(remoteMailbox), block);
     assertSame(capturedBlock(_mailbox1), block);
     assertNotSame(capturedBlock(_mailbox2), block);
@@ -143,12 +146,52 @@ public class BroadcastExchangeTest {
     inOrder.verify(_mailbox1).send(Mockito.any(MseBlock.Data.class));
   }
 
+  @Test
+  public void shouldNotCopyBlocksForSpoolStagesWithoutByReferenceWorkers() {
+    // Given: two receiver stages of a multi-send (spool) node, each fanning 
out to workers on other servers
+    SendingMailbox remoteMailbox1 = remoteMailbox();
+    SendingMailbox remoteMailbox2 = remoteMailbox();
+    RowHeapDataBlock block = funnelBlock(stepEvents(new FunnelStepEvent(1000L, 
0)));
+
+    // When:
+    route(block, spoolStageMailbox(remoteMailbox1), 
spoolStageMailbox(remoteMailbox2));
+
+    // Then: no copies are made, because every worker of both stages 
serializes the block
+    Mockito.verify(_aggFunction, 
Mockito.never()).serializeIntermediateResult(Mockito.any());
+    assertSame(capturedBlock(remoteMailbox1), block);
+    assertSame(capturedBlock(remoteMailbox2), block);
+  }
+
+  @Test
+  public void shouldCopyBlocksForSpoolStagesWithAByReferenceWorker() {
+    // Given: two receiver stages of a multi-send (spool) node. The second one 
has a worker on this server, next to
+    // a worker on another server
+    SendingMailbox remoteMailbox = remoteMailbox();
+    RowHeapDataBlock block = funnelBlock(stepEvents(new FunnelStepEvent(1000L, 
0)));
+
+    // When:
+    route(block, spoolStageMailbox(_mailbox1), 
spoolStageMailbox(remoteMailbox, _mailbox2));
+
+    // Then: one copy is made, because a single by-reference worker makes the 
whole stage share the block
+    Mockito.verify(_aggFunction, 
Mockito.times(1)).serializeIntermediateResult(Mockito.any());
+    assertSame(capturedBlock(_mailbox1), block);
+    MseBlock.Data copiedBlock = capturedBlock(_mailbox2);
+    assertNotSame(copiedBlock, block);
+    // Within that stage the copy is shared again: only one of its workers 
keeps a reference to it
+    assertSame(capturedBlock(remoteMailbox), copiedBlock);
+    // The original block reaches its receiver last, after every other 
destination has read it
+    InOrder inOrder = Mockito.inOrder(remoteMailbox, _mailbox2, _mailbox1);
+    inOrder.verify(remoteMailbox).send(Mockito.any(MseBlock.Data.class));
+    inOrder.verify(_mailbox2).send(Mockito.any(MseBlock.Data.class));
+    inOrder.verify(_mailbox1).send(Mockito.any(MseBlock.Data.class));
+  }
+
   @Test
   @SuppressWarnings("unchecked")
   public void shouldShareBlocksWithObjectColumnsWithRemoteOnlyDestinations() {
     // Given: only remote destinations, which serialize the block instead of 
delivering it by reference
-    SendingMailbox remoteMailbox1 = Mockito.mock(SendingMailbox.class);
-    SendingMailbox remoteMailbox2 = Mockito.mock(SendingMailbox.class);
+    SendingMailbox remoteMailbox1 = remoteMailbox();
+    SendingMailbox remoteMailbox2 = remoteMailbox();
     RowHeapDataBlock block = funnelBlock(stepEvents(new FunnelStepEvent(1000L, 
0)));
 
     // When:
@@ -255,6 +298,20 @@ public class BroadcastExchangeTest {
     new BroadcastExchange(destinationList, 
BlockSplitter.NO_OP).route(destinationList, block);
   }
 
+  /// Wraps the mailboxes of one receiver stage the way a multi-send (spool) 
node does: an inner exchange per stage,
+  /// exposed to the outer exchange as a single sending mailbox.
+  private static SendingMailbox spoolStageMailbox(SendingMailbox... 
innerMailboxes) {
+    return new BroadcastExchange(List.of(innerMailboxes), 
BlockSplitter.NO_OP).asSendingMailbox("1");
+  }
+
+  /// A mailbox to a worker on another server. It reads the block within send 
instead of giving it to the receiver.
+  private static SendingMailbox remoteMailbox() {
+    SendingMailbox mailbox = Mockito.mock(SendingMailbox.class);
+    Mockito.when(mailbox.isLocal()).thenReturn(false);
+    Mockito.when(mailbox.deliversByReference()).thenReturn(false);
+    return mailbox;
+  }
+
   private static MseBlock.Data capturedBlock(SendingMailbox mailbox) {
     ArgumentCaptor<MseBlock.Data> captor = 
ArgumentCaptor.forClass(MseBlock.Data.class);
     Mockito.verify(mailbox, Mockito.times(1)).send(captor.capture());


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

Reply via email to