Account a mailbox send's current block before it reports its stats - #19365
Merged
Conversation
A mailbox send operator serializes its stats into the end-of-stream block it
sends, so it collects them from inside the getNextBlock() call it is still
running. nextBlock() only registers a block's usage once getNextBlock() has
returned, so what that call has spent is missing from what the operator reports.
Because the send's call contains the call it made to its input, the operator
ends up reporting less than its own children, and the stats tree renders a
negative self time for the stage:
MAILBOX_SEND (no executionTimeMs) selfExecutionTimeMs: -310
AGGREGATE executionTimeMs: 310
It is worst when a stage handles no data block at all, since the end-of-stream
block is then the only block and the operator reports nothing, but the value is
understated by its last block in every case. Memory and GC time are captured at
the same point and are understated the same way, so selfAllocatedMB and
selfGcTimeMs can go negative too.
MultiStageOperator now keeps what the running getNextBlock() call has spent in a
BlockExecution object, non-null exactly while such a call is running, and
registers only what has not been accounted yet when the call returns.
registerExecutionSoFar() lets an operator account the call from the inside;
MailboxSendOperator calls it just before collecting its stats. The totals an
operator ends up with are unchanged, so the metrics fed from them are unaffected.
The self time is now non-negative by construction, because the stats are
collected strictly after every input call has returned. The send remains short
by the time it spends serializing the stats and handing the block to the
exchange, which cannot be counted without moving the stats out of the block.
Stats reported directly to the broker were already correct: that path collects
them after the opchain loop has finished, and does not go through sendEos().
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #19365 +/- ##
============================================
- Coverage 67.20% 67.16% -0.04%
- Complexity 1418 1424 +6
============================================
Files 3479 3479
Lines 223075 223092 +17
Branches 35135 35136 +1
============================================
- Hits 149920 149847 -73
- Misses 61184 61286 +102
+ Partials 11971 11959 -12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
yashmayya
approved these changes
Aug 25, 2026
xiangfu0
added a commit
to pinot-contrib/pinot-docs
that referenced
this pull request
Aug 26, 2026
Documents the streaming stage-stats correction from apache/pinot#19365. - explain when MAILBOX_SEND accounts its final call - clarify non-negative derived self metrics - record the remaining snapshot boundary and upgrade impact Upstream: apache/pinot#19365 Co-authored-by: Xiang Fu <xiangfu@Xiang-mac-mtv-2.local>
Contributor
|
Documentation follow-up: pinot-contrib/pinot-docs#1008 (merged). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A mailbox send operator serializes its stats into the end-of-stream block it sends, so it collects them from inside the
getNextBlock()call it is still running.nextBlock()only registers a block's usage oncegetNextBlock()has returned:Since the send's call contains the call it made to its input, it ends up reporting less than its own children, and
selfExecutionTimeMs = parent − Σ childrengoes negative:It is most visible when a stage handles no data block at all — the end-of-stream block is then the only block, so the operator reports nothing — but the value is understated by its final block in every case.
memoryUsedBytesandgcTimeMsare captured at the same point and are understated the same way, soselfAllocatedMBandselfGcTimeMscan go negative too.Measured directly: a send whose input takes ~300ms and then reports EOS serializes
executionTimeMs: 0while the call actually took 291ms.Fix
MultiStageOperatorkeeps what the runninggetNextBlock()call has spent in aBlockExecutionobject, non-null exactly while such a call is running, and registers only what has not been accounted yet when the call returns.registerExecutionSoFar()lets an operator account the call from the inside;MailboxSendOperatorcalls it just before collecting its stats.Each registration contributes only what accrued since the previous one, so the totals an operator ends up with are unchanged —
OpChainSchedulerService.onOpChainFinishedand theCPU_EXECUTION_TIME_MSmeter see exactly what they saw before. Only the serialized snapshot moves earlier.Same query after the fix:
The self time is now non-negative by construction: the stats are collected strictly after every input call has returned, so the send's elapsed time necessarily covers its children's.
What this does not fix
The send remains short by the time it spends serializing the stats and handing the block to the exchange, after the snapshot. Counting that would require the stats to leave the operator outside
getNextBlock(), which is a protocol change.Stats reported directly to the broker were already correct and are untouched:
QueryRunner.effectiveSendStats()returns false in stream mode, so that path never goes throughsendEos()and collects its stats after the opchain loop has finished.Testing
MailboxSendOperatorTest.shouldAccountCurrentBlockBeforeReportingStats— an input that takes time and then reports EOS with no data block, so the end-of-stream block is the only one. Asserts the stats handed to the exchange carry a non-zero time, and that the total does not exceed the wall time of the call, which is what a double count would look like.QueryRunnerTest.testSelfStatsAreNotNegative— asserts noselfExecutionTimeMs,selfClockTimeMs,selfAllocatedMBorselfGcTimeMsanywhere in the rendered stats tree is negative, over a real two-server query whose filter matches nothing.Both were verified to fail without the fix (
got 0andselfExecutionTimeMs is -310respectively).Relationship to #19364
Independent — this touches only when stats are read, not what they contain. The negative self time was noticed while working on that PR.