gortiz opened a new pull request, #19066:
URL: https://github.com/apache/pinot/pull/19066

   ## Problem
   
   `StreamingGroupByCombineOperator` can crash with `IndexOutOfBoundsException` 
(or
   silently return wrong aggregates) on high-cardinality group-by queries.
   
   The operator's worker threads produce a per-segment `GroupByResultsBlock` 
and hand
   it to the consumer thread through the blocking queue. When a block carries a 
raw
   `AggregationGroupByResult`, that result is backed by the producing worker 
thread's
   `DictionaryBasedGroupKeyGenerator` group-key map — which is **thread-local 
and
   reused**. After enqueuing the block, the same worker advances to its next 
segment
   and `clearAndTrim()`/`expand()`s that thread-local map, while the consumer 
thread is
   still iterating the earlier block in `mergeBlock` →
   `AggregationGroupByResult.getResultForGroupId(...)`. The torn read produces a
   corrupted group id that indexes the segment's result holder out of bounds, 
e.g.:
   
   ```
   Caught exception while streaming group-by results:
   Index 524288 out of bounds for length 524288
   ```
   
   ### When it triggers
   - The segment's group cardinality exceeds `maxInitialResultHolderCapacity`
     (default `10000`). Below that, `DictionaryBasedGroupKeyGenerator` uses a
     per-instance `ArrayBasedHolder` instead of the reused thread-local 
`IntGroupIdMap`,
     so there is no aliasing.
   - `streamingGroupByFlushThreshold` is set, so group-by is routed through
     `StreamingGroupByCombineOperator`.
   
   The non-streaming `GroupByCombineOperator` is **not** affected: it merges 
each
   segment inline on the producing worker thread, before that thread reuses its
   thread-local map.
   
   ## Fix
   
   Materialize each raw per-segment `AggregationGroupByResult` into 
self-contained
   `IntermediateRecord`s **on the producing worker thread**, before the block 
is handed
   to the consumer — mirroring how the non-streaming operator merges inline.
   
   - New `BaseStreamingCombineOperator.detachFromWorkerThreadState(T)` hook 
(identity by
     default), invoked in `processSegments` before each block is enqueued.
   - `StreamingGroupByCombineOperator` overrides it to iterate the group-key 
iterator,
     extract results, and rebuild a block backed only by `IntermediateRecord`s. 
The
     consumer-side `mergeBlock` then only ever consumes self-contained records.
   - Adds `IntermediateRecord.withoutOrderByValues(Key, Record)` (the 
constructor is
     package-private and the streaming operator lives in a different package).
   - The selection-only streaming combine operator keeps the no-op default.
   
   ## Testing
   
   Adds 
`StreamingGroupByCombineOperatorTest#testHighCardinalityConcurrentMergeIsCorrect`
   (16 segments × 20000 groups, rows shuffled per segment so each segment's 
group-id↔key
   mapping differs, `maxExecutionThreads=1`). It reliably reproduces the crash 
on the
   current code and passes with the fix. Existing tests in the class continue 
to pass.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


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