Repository navigation
ES|QL: Integrate circuit breaker into BestBucketsDeferringCollector - #155600
hawkhuang-collab merged 30 commits into
Conversation
|
Hi @hawkhuang-collab, I've created a changelog YAML for you. |
🔍 Preview links for changed docs⏳ Building and deploying preview... View progress This comment will be updated with preview links when the build is complete. |
ℹ️ Important: Docs version tagging👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version. We use applies_to tags to mark version-specific features and changes. Expand for a quick overviewWhen to use applies_to tags:✅ At the page level to indicate which products/deployments the content applies to (mandatory) What NOT to do:❌ Don't remove or replace information that applies to an older version 🤔 Need help?
|
|
Pinging @elastic/es-analytical-engine (Team:Analytics) |
BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue elastic#148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: elastic#148516
Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection.
- Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out.
Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4).
Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance
There was a problem hiding this comment.
Is this worth a cranky breaker test? Not sure how/if we test with cranky breaker other aggs, but I would give it a look. This feels safe as it uses addRequestCircuitBreakerBytes though
There was a problem hiding this comment.
It's worth making sure we have cranky tests, yeah. We wrote the first cranky breaker for aggs, so it should be available. We might even get it by default.
There was a problem hiding this comment.
I'll look into this.
There was a problem hiding this comment.
Good insight and time for me to search and learn cranky breaker tests! :)
There was a problem hiding this comment.
Ok, so this is a design trade off/ design decision, and need your expert suggestion.
In our current implementation, we use LongConsumer, which is more flexible and decoupled and we used in many existing aggregator such as: MapStringTermsAggregator, the downside/ problem is that then we can't use the existing test coverage as it has to use CircuitBreaker directly..
There was a problem hiding this comment.
I've added a manual cranky test there for now.
| lastDoc = doc; | ||
| // Periodically give the real-memory parent breaker a chance to check heap | ||
| // usage for in-flight builder allocations that are not yet committed to entries. | ||
| if ((++callCount & 0x3FF) == 0) { |
There was a problem hiding this comment.
Can we avoid the magic bitwise operations? The compiler should be smart enough to figure this out on its own :) and if can't for whatever (verified) reason, this needs a comment explaining what it's doing.
There was a problem hiding this comment.
Yeah, make sense, will update it.
There was a problem hiding this comment.
Meh. I read this as "This fires every thousand of so calls and that caller cares very much about this line being fast."
There was a problem hiding this comment.
My bitwise-fu is very, very weak. I see something like this and I immediately go cross-eyed like Austin Powers trying to understand time-travel. Even if this was warranted, a simple comment like what you just said would suffice.
There was a problem hiding this comment.
Thanks for the feedback, I'll keep the bitwise check since this is a hot-path optimization, and I'll add a comment explaining that it's intentionally checking every 1024 calls.
| * aggregator's tracker means bytes flow through {@code requestBytesUsed} and are automatically | ||
| * returned when the aggregator is closed, even if the query fails before | ||
| * {@link #prepareSelectedBuckets} is called. | ||
| */ |
There was a problem hiding this comment.
The javadoc for this this is repeated (i none form or another) multiple times. And either way, is it really this class's responsibility how this is used? Perhaps just giving this a more descriptive name should suffice.
There was a problem hiding this comment.
Let me remove the javadoc, I think the name is already self explained enough, and yes, this is the right place and right type as well.
| * Sole constructor. | ||
| * @param isGlobal Whether this collector visits all documents (global context) | ||
| * @param bytesAccounter Callback used to charge and return circuit breaker bytes. | ||
| * Pass {@code aggregator::addRequestCircuitBreakerBytes} so that |
There was a problem hiding this comment.
This javadoc feels very smelly to me. If you expect this to be a very specific callback, then it should be enforced in more than just a Javadoc comment.
| // Charge the circuit breaker for the packed values we just committed. If this | ||
| // trips, the exception propagates out of getLeafCollector and the aggregator | ||
| // framework handles cleanup via AggregatorBase.close(). | ||
| bytesAccounter.accept(entry.ramBytesUsed); |
There was a problem hiding this comment.
Comment would be redundant if bytesAccounter had a better name, and either way, this feels like a very "The AI wrote this code to explain to me why it chose the solution it did, but there's no reason to keep this for posterity" kind of comment :)
There was a problem hiding this comment.
You got it.
| // second loop, entries still points at the old list whose bytes have already | ||
| // been returned; the resulting CircuitBreakingException propagates to the | ||
| // caller, failing the query, and AggregatorBase.close() reconciles | ||
| // requestBytesUsed with the breaker on the way out. |
There was a problem hiding this comment.
Same thing here: feels like "AI is explaining their solution" more than a comment that should be kept for all time.
There was a problem hiding this comment.
Let me update it
| for (int i = 0; i < 5; i++) { | ||
| indexWriter.addDocument(new Document()); | ||
| } | ||
| } |
There was a problem hiding this comment.
These tests are very long long and duplicate a lot of their setup. Could you please refactor this so it's DRYer and also easier to review?
There was a problem hiding this comment.
Yeah, fair point, let me work on it.
e96dc75 to
c7a8156
Compare
GalLalouche
left a comment
There was a problem hiding this comment.
LGTM with a few remaining suggestions. Thanks @hawkhuang-collab!
…lastic#155600) * Integrate circuit breaker into BestBucketsDeferringCollector BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue elastic#148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: elastic#148516 * Update docs/changelog/155600.yaml * Test circuit breaker accounting in BestBucketsDeferringCollector Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection. * Polish BestBucketsDeferringCollector: null-check and comment accuracy - Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out. * [CI] Auto commit changes from spotless * Strengthen circuit breaker tests to verify event symmetry Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4). * Add edge-case circuit breaker tests for BestBucketsDeferringCollector Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance * Address review comments on BestBucketsDeferringCollector * Refactor circuit breaker tests to reduce duplication * simplify to inline. * Add cranky circuit breaker test for BestBucketsDeferringCollector * Extract runBreakerTest helper to eliminate index setup boilerplate * [CI] Auto commit changes from spotless * Extract withSearcher helper for custom-accounter tests * Polish cranky test and CollectingBucketCollector * Trim finishLeaf comment to the non-obvious why * Assert byte balance after circuit breaker trips in tests * Remove unused default from withSearcher * Move zero-sum invariant into runBreakerTest and make helpers static * Clarify accept(0) heartbeat protocol in collect loop * remove ghost * Extract heartbeat() method from getLeafCollector --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
…155600) (#156781) * Integrate circuit breaker into BestBucketsDeferringCollector BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue #148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: #148516 * Update docs/changelog/155600.yaml * Test circuit breaker accounting in BestBucketsDeferringCollector Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection. * Polish BestBucketsDeferringCollector: null-check and comment accuracy - Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out. * [CI] Auto commit changes from spotless * Strengthen circuit breaker tests to verify event symmetry Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4). * Add edge-case circuit breaker tests for BestBucketsDeferringCollector Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance * Address review comments on BestBucketsDeferringCollector * Refactor circuit breaker tests to reduce duplication * simplify to inline. * Add cranky circuit breaker test for BestBucketsDeferringCollector * Extract runBreakerTest helper to eliminate index setup boilerplate * [CI] Auto commit changes from spotless * Extract withSearcher helper for custom-accounter tests * Polish cranky test and CollectingBucketCollector * Trim finishLeaf comment to the non-obvious why * Assert byte balance after circuit breaker trips in tests * Remove unused default from withSearcher * Move zero-sum invariant into runBreakerTest and make helpers static * Clarify accept(0) heartbeat protocol in collect loop * remove ghost * Extract heartbeat() method from getLeafCollector --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
…lastic#155600) * Integrate circuit breaker into BestBucketsDeferringCollector BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue elastic#148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: elastic#148516 * Update docs/changelog/155600.yaml * Test circuit breaker accounting in BestBucketsDeferringCollector Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection. * Polish BestBucketsDeferringCollector: null-check and comment accuracy - Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out. * [CI] Auto commit changes from spotless * Strengthen circuit breaker tests to verify event symmetry Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4). * Add edge-case circuit breaker tests for BestBucketsDeferringCollector Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance * Address review comments on BestBucketsDeferringCollector * Refactor circuit breaker tests to reduce duplication * simplify to inline. * Add cranky circuit breaker test for BestBucketsDeferringCollector * Extract runBreakerTest helper to eliminate index setup boilerplate * [CI] Auto commit changes from spotless * Extract withSearcher helper for custom-accounter tests * Polish cranky test and CollectingBucketCollector * Trim finishLeaf comment to the non-obvious why * Assert byte balance after circuit breaker trips in tests * Remove unused default from withSearcher * Move zero-sum invariant into runBreakerTest and make helpers static * Clarify accept(0) heartbeat protocol in collect loop * remove ghost * Extract heartbeat() method from getLeafCollector --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
…155600) (#156780) * Integrate circuit breaker into BestBucketsDeferringCollector BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue #148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: #148516 * Update docs/changelog/155600.yaml * Test circuit breaker accounting in BestBucketsDeferringCollector Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection. * Polish BestBucketsDeferringCollector: null-check and comment accuracy - Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out. * [CI] Auto commit changes from spotless * Strengthen circuit breaker tests to verify event symmetry Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4). * Add edge-case circuit breaker tests for BestBucketsDeferringCollector Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance * Address review comments on BestBucketsDeferringCollector * Refactor circuit breaker tests to reduce duplication * simplify to inline. * Add cranky circuit breaker test for BestBucketsDeferringCollector * Extract runBreakerTest helper to eliminate index setup boilerplate * [CI] Auto commit changes from spotless * Extract withSearcher helper for custom-accounter tests * Polish cranky test and CollectingBucketCollector * Trim finishLeaf comment to the non-obvious why * Assert byte balance after circuit breaker trips in tests * Remove unused default from withSearcher * Move zero-sum invariant into runBreakerTest and make helpers static * Clarify accept(0) heartbeat protocol in collect loop * remove ghost * Extract heartbeat() method from getLeafCollector --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
…lastic#155600) * Integrate circuit breaker into BestBucketsDeferringCollector BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue elastic#148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: elastic#148516 * Update docs/changelog/155600.yaml * Test circuit breaker accounting in BestBucketsDeferringCollector Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection. * Polish BestBucketsDeferringCollector: null-check and comment accuracy - Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out. * [CI] Auto commit changes from spotless * Strengthen circuit breaker tests to verify event symmetry Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4). * Add edge-case circuit breaker tests for BestBucketsDeferringCollector Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance * Address review comments on BestBucketsDeferringCollector * Refactor circuit breaker tests to reduce duplication * simplify to inline. * Add cranky circuit breaker test for BestBucketsDeferringCollector * Extract runBreakerTest helper to eliminate index setup boilerplate * [CI] Auto commit changes from spotless * Extract withSearcher helper for custom-accounter tests * Polish cranky test and CollectingBucketCollector * Trim finishLeaf comment to the non-obvious why * Assert byte balance after circuit breaker trips in tests * Remove unused default from withSearcher * Move zero-sum invariant into runBreakerTest and make helpers static * Clarify accept(0) heartbeat protocol in collect loop * remove ghost * Extract heartbeat() method from getLeafCollector --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
…lastic#155600) * Integrate circuit breaker into BestBucketsDeferringCollector BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue elastic#148516: 5.8GB of 8GB observed). Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure. - finishLeaf(): charge ramBytesUsed of each committed entry - prepareSelectedBuckets(): return bytes as entry data is freed - rewriteBuckets(): return old entry bytes, charge new entry bytes - collect(): periodic zero-byte check lets the parent real-memory breaker catch in-flight builder growth between finishLeaf calls Fixes: elastic#148516 * Update docs/changelog/155600.yaml * Test circuit breaker accounting in BestBucketsDeferringCollector Add three tests that exercise the byte-tracking added in the parent commit: - testCircuitBreakerBytesChargedAndReturnedAfterReplay: verifies that the running balance is positive after collection and reaches zero after prepareSelectedBuckets returns all entry bytes. - testCircuitBreakerBytesAdjustedByRewriteBuckets: verifies that rewriteBuckets returns old entry bytes and recharges for rebuilt entries, with the balance still reaching zero after replay. - testCircuitBreakerTripDuringFinishLeaf: verifies that a CircuitBreakingException thrown by the bytesAccounter propagates correctly out of postCollection. * Polish BestBucketsDeferringCollector: null-check and comment accuracy - Add Objects.requireNonNull for bytesAccounter in the constructor so a missing callback surfaces immediately at construction rather than as a NullPointerException deep inside finishLeaf. - Expand the rewriteBuckets comment to accurately describe how long the old packed structures remain live (the full rebuild loop, not just a brief moment) and document the exception-safety property: a CircuitBreakingException mid-rebuild leaves entries pointing at the old list, fails the query, and lets AggregatorBase.close() reconcile the breaker on the way out. * [CI] Auto commit changes from spotless * Strengthen circuit breaker tests to verify event symmetry Replace the AtomicLong-based tests with event-list tests that capture every individual charge and return call: - testCircuitBreakerChargesOneEventPerSegmentAndReleasesSymmetrically: asserts exactly one positive event per segment after collection, and that each subsequent negative event equals the exact negation of its corresponding charge (not just that the final sum is zero). - testCircuitBreakerRewriteBucketsProducesSymmetricEvents: asserts the full 6-event sequence [+A, -A, +A', +B, -A', -B], including that the rebuilt entry A' is smaller than A (all-zero bucket array compresses better than distinct ordinals 0-4). * Add edge-case circuit breaker tests for BestBucketsDeferringCollector Cover six boundary conditions not exercised by the existing tests: - empty segment (no collect() calls) produces no CB event - prepareSelectedBuckets with no matching ordinals still returns bytes - rewriteBuckets on empty entries fires no events - rewriteBuckets mapping all ordinals to -1 returns bytes without recharging - two successive rewriteBuckets calls maintain symmetric accounting - zero-byte heartbeat events (>1024 docs) don't affect the net balance * Address review comments on BestBucketsDeferringCollector * Refactor circuit breaker tests to reduce duplication * simplify to inline. * Add cranky circuit breaker test for BestBucketsDeferringCollector * Extract runBreakerTest helper to eliminate index setup boilerplate * [CI] Auto commit changes from spotless * Extract withSearcher helper for custom-accounter tests * Polish cranky test and CollectingBucketCollector * Trim finishLeaf comment to the non-obvious why * Assert byte balance after circuit breaker trips in tests * Remove unused default from withSearcher * Move zero-sum invariant into runBreakerTest and make helpers static * Clarify accept(0) heartbeat protocol in collect loop * remove ghost * Extract heartbeat() method from getLeafCollector --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
Fixes: #148516
BestBucketsDeferringCollector stores PackedLongValues (doc deltas and bucket ordinals) for every collected document with no circuit breaker tracking. Under high-cardinality deferring aggregations (rare-terms, auto-date-histogram, variable-width-histogram), this can exhaust heap (issue #148516: 5.8GB of 8GB observed).
Add a LongConsumer bytesAccounter to the constructor. Callers pass this::addRequestCircuitBreakerBytes from the owning aggregator so bytes flow through requestBytesUsed and are automatically returned when the aggregator closes, even on query failure.