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

xiangfu0 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 a2a27a1e514 Fix flaky SelectionCombineOperatorTest by waiting for 
workers before reading stats (#18946)
a2a27a1e514 is described below

commit a2a27a1e5146fe6687f5be5de038743f2bdbbfbd
Author: Timothy Elgersma <[email protected]>
AuthorDate: Sat Jul 11 21:26:41 2026 -0400

    Fix flaky SelectionCombineOperatorTest by waiting for workers before 
reading stats (#18946)
    
    * Fix flaky SelectionCombineOperatorTest by waiting for workers before 
reading stats
    
    In `BaseSingleBlockCombineOperator.getNextBlock()`, 
`attachExecutionStats()` was called before `stopProcess()`, meaning worker 
threads could still be mutating operator state (specifically `_numDocsScanned` 
in `SelectionOnlyOperator`, a plain non-volatile `int`) when the main thread 
iterated over operators to aggregate statistics. This caused 
`numEntriesScannedPostFilter` (derived as `_numDocsScanned * 
numColumnsProjected`) to diverge from `numDocsScanned` when a worker thread 
updated  [...]
    
    The fix restructures `getNextBlock()` so that `stopProcess()` — which 
already uses a `Phaser` to await all worker threads — runs in the `finally` 
block before `attachExecutionStats()` is called. No behavior change on the 
normal (non-early-termination) path.
    
    Fix flakey test.
    
    Reproduced the flake by adding `@Test(invocationCount = 5000)` to 
`selectionOnly()` and running against the pre-fix code: failed twice with 
`expected [30] but found [20]` and `expected [50] but found 
[40]`(numEntriesScannedPostFilter ≠ numDocsScanned). The same 5000-iteration 
run passes cleanly with the fix applied.
    
    Safe to revert. The change only reorders when `stopProcess()` is called 
relative to `attachExecutionStats()` within a single query execution; it has no 
effect on query results or external behavior, only on the correctness of 
execution statistics under early termination.
    
    * wrap the whole body in another try catch to avoid throwing anything at 
all.
---
 .../combine/BaseSingleBlockCombineOperator.java    | 24 ++++++++++++++++++++--
 .../combine/CombineErrorOperatorsTest.java         | 21 +++++++++++++++++++
 2 files changed, 43 insertions(+), 2 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseSingleBlockCombineOperator.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseSingleBlockCombineOperator.java
index 9713cc1a8b4..56348995a63 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseSingleBlockCombineOperator.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/BaseSingleBlockCombineOperator.java
@@ -59,14 +59,34 @@ public abstract class BaseSingleBlockCombineOperator<T 
extends BaseResultsBlock>
   /// Handles exceptions here so that execution stats can be attached.
   @Override
   protected BaseResultsBlock getNextBlock() {
+    try {
+      return mergeResultsAndAttachExecutionStats();
+    } catch (Exception e) {
+      LOGGER.error("Caught exception while attaching execution stats (query: 
{})", _queryContext, e);
+      QueryErrorMessage errMsg =
+          QueryErrorMessage.safeMsg(QueryErrorCode.INTERNAL, "Caught exception 
while attaching execution stats");
+      return new ExceptionResultsBlock(errMsg);
+    }
+  }
+
+  /// Merges the results and waits for all worker threads to finish before 
attaching execution statistics.
+  private BaseResultsBlock mergeResultsAndAttachExecutionStats() {
+    BaseResultsBlock mergedBlock = null;
+    Exception mergeException = null;
     try {
       startProcess();
-      return checkTerminateExceptionAndAttachExecutionStats(mergeResults());
+      mergedBlock = mergeResults();
     } catch (Exception e) {
-      return createExceptionResultsBlockAndAttachExecutionStats(e, "merging 
results blocks");
+      mergeException = e;
     } finally {
+      // Wait for all worker threads to finish before reading execution stats. 
This ensures that no worker thread is
+      // still mutating operator state (e.g. _numDocsScanned) when 
attachExecutionStats() iterates over operators.
       stopProcess();
     }
+    if (mergeException != null) {
+      return 
createExceptionResultsBlockAndAttachExecutionStats(mergeException, "merging 
results blocks");
+    }
+    return checkTerminateExceptionAndAttachExecutionStats(mergedBlock);
   }
 
   @Override
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/CombineErrorOperatorsTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/CombineErrorOperatorsTest.java
index 5d8eec88c98..4c51829cecd 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/CombineErrorOperatorsTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/CombineErrorOperatorsTest.java
@@ -128,6 +128,19 @@ public class CombineErrorOperatorsTest {
     assertEquals(errorMsg.getErrCode(), QueryErrorCode.QUERY_EXECUTION);
   }
 
+  @Test
+  public void testCombineExecutionStatisticsException() {
+    SelectionOnlyCombineOperator combineOperator =
+        new SelectionOnlyCombineOperator(List.of(new 
ExecutionStatisticsExceptionOperator()), QUERY_CONTEXT,
+            _executorService);
+    BaseResultsBlock resultsBlock = combineOperator.nextBlock();
+    assertTrue(resultsBlock instanceof ExceptionResultsBlock);
+    List<QueryErrorMessage> errorMsgs = resultsBlock.getErrorMessages();
+    assertNotNull(errorMsgs);
+    assertEquals(errorMsgs.size(), 1);
+    assertEquals(errorMsgs.get(0).getErrCode(), QueryErrorCode.INTERNAL);
+  }
+
   @Test(dataProvider = "getErrorCodes")
   public void testCombineExceptionAndErrorOperator(QueryErrorCode 
queryErrorCode) {
     List<Operator> operators = new ArrayList<>(NUM_OPERATORS);
@@ -227,6 +240,14 @@ public class CombineErrorOperatorsTest {
     }
   }
 
+  /// Operator that successfully produces a block but fails while reporting 
execution statistics.
+  private static class ExecutionStatisticsExceptionOperator extends 
RegularOperator {
+    @Override
+    public ExecutionStatistics getExecutionStatistics() {
+      throw new RuntimeException("Failed to retrieve execution statistics");
+    }
+  }
+
   private static class RegularGroupByOperator extends BaseOperator {
     private static final String EXPLAIN_NAME = "REGULAR_GROUP_BY";
 


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

Reply via email to