Skip to content

[9.5] ES|QL:Fix Tdigest percentiles circuit breaker (#155586) - #156858

Merged
elasticsearchmachine merged 1 commit into
elastic:9.5from
hawkhuang-collab:backport/9.5/pr-155586
Aug 15, 2026
Merged

[9.5] ES|QL:Fix Tdigest percentiles circuit breaker (#155586)#156858
elasticsearchmachine merged 1 commit into
elastic:9.5from
hawkhuang-collab:backport/9.5/pr-155586

Conversation

@hawkhuang-collab

Copy link
Copy Markdown
Contributor

Backports the following commits to 9.5:

* Integrate circuit breaker into TDigest percentiles agg

`AbstractTDigestPercentilesAggregator` was creating every per-bucket
`HistogramUnionState` with `NOOP_BREAKER`, making all TDigest memory
invisible to the request circuit breaker. Under high-cardinality
`TERMS` + `PERCENTILES` queries the heap could fill and OOM the node
instead of the breaker throwing `CircuitBreakingException` and
rejecting the query gracefully.

Pass `context.breaker()` so the full tracking chain activates:
`HistogramUnionState` -> `TDigestState` -> `MemoryTrackingTDigestArrays`,
which charges every centroid array allocation against the REQUEST breaker.

Also fix `doClose()` to close each `HistogramUnionState` individually
before releasing the backing array, so the bytes charged at creation
are returned to the breaker rather than leaked in its accounting.

Closes elastic#99815 (_search agg path)

* Revert unsafe doClose; add circuit breaker test

Remove element-level close from doClose: HistogramUnionState is
aliased by the InternalAggregation built in buildAggregation, and
AggregatorCollector calls releaseAggregations() immediately after
buildTopLevel(), making element-level close unsafe. Bytes from
collection states are not explicitly returned; that requires
making InternalTDigestPercentiles Releasable (deferred, elastic#99815).

Add testCircuitBreakerTripsOnHighCardinality to verify the REQUEST
breaker trips during collection when centroid arrays exceed the
configured limit, confirming the context.breaker() fix works.

* fix reduce-path crash: use NOOP_BREAKER in getLeaderReducer

The HistogramUnionState built during collection holds a reference to
the PreallocatedCircuitBreaker from the aggregation context. That
breaker is closed when the context closes (after buildTopLevel()),
before the reduce phase runs on the coordinator.

When getLeaderReducer() called createUsingParamsFrom(percentiles.state)
it inherited the now-closed breaker, causing
  IllegalStateException: already closed
at addEstimateBytesAndMaybeBreak inside TDigestState.createUsingParamsFrom.

Fix: add a breaker-explicit overload to both TDigestState and
HistogramUnionState createUsingParamsFrom, and pass NOOP_BREAKER when
creating the reduce-phase accumulator. Wire-deserialized states already
used NOOP_BREAKER; this brings local-reduce into parity. The merged
accumulator is never closed, so NOOP_BREAKER is correct here.

* fix forbidden API: replace MatchAllDocsQuery with Queries.ALL_DOCS_INSTANCE

testCircuitBreakerTripsOnHighCardinality used new MatchAllDocsQuery()
directly; ES policy requires Queries.ALL_DOCS_INSTANCE instead.

* fix doClose: null-out states after buildAggregation, close elements

buildAggregation() previously returned a live HistogramUnionState
reference to InternalTDigestPercentiles while doClose() would also
attempt to close it (via Releasables.close(states) which only closes
the ObjectArray container, not its elements -- but the element-level
close was missing entirely).

Add takeState() to the base class: atomically reads and nulls out a
slot, transferring ownership to the caller. Both buildAggregation()
overrides now use takeState() instead of getState() so doClose() can
no longer double-close a state that was handed off successfully.

doClose() now iterates over the remaining (non-null) slots and closes
each one individually before closing the container. This ensures
circuit-breaker bytes are returned on the failure path -- i.e. when
a CircuitBreakingException aborts collection before buildAggregation
is ever called, leaked states are released rather than left charged
against the breaker until the parent breaker resets.

Also guard against states == null in doClose(): a cranky circuit
breaker can trip inside the constructor before the field is assigned.

* fix takeState byte release and reducer swap

Release breaker bytes in takeState() while the aggregation
context is still open — the only safe window before the
PreallocatedCircuitBreaker closes. Replace merge() swap
logic with merged.add() to prevent the accumulator from
inheriting a shard-side breaker that may already be closed.
Use closeWhileHandlingException in doClose() so a failure
on one slot does not prevent the remaining slots and the
container from being released.

* add breaker-balance assert; null-check doClose slots

Verify that doClose() returns all partial breaker bytes after
CircuitBreakingException by asserting the REQUEST breaker is
at zero after expectThrows. Add explicit null check inside the
doClose loop so taken slots are visibly skipped rather than
relying on Releasables null-tolerance.

* clean up comments and simplify test boilerplate

Remove diff-anchored and redundant comments; fix em dashes in
Javadoc; trim doClose block comment to two lines; extract
requestBreakerService() helper to reduce test setup boilerplate.

* add cranky breaker test; extract withSequentialIndex/collectWithBreaker helpers

* restore merge() helper; only the NOOP_BREAKER initialisation needed to change

* Update docs/changelog/155586.yaml

* Update docs/changelog/155586.yaml

* update comments.

* address review feedback

- @nullable + final on takeState(); document null cases
- doClose(): early exit, drop redundant null guard in loop,
  remove try/finally (closeWhileHandlingException never rethrows)
- NOOP_BREAKER comment: honest about coordinator gap + follow-up
- remove redundant @param/@return from createUsingParamsFrom javadoc
- rename tests; add terms+percentiles high-cardinality trip test;
  drop cranky test (coverage held by deterministic trip tests)

* spotless: remove final from takeState()

* [CI] Auto commit changes from spotless

* simplify NOOP_BREAKER comment in getLeaderReducer

* fix NOOP_BREAKER comment: describe use-after-close problem

* update comments.

* fix comment: NOOP_BREAKER is for unreleased accumulator

* fix comment: explain both reasons for NOOP_BREAKER

* simplify comments in AbstractTDigestPercentilesAggregator

* simplify NOOP_BREAKER comment in getLeaderReducer

* address review: static helper, drop redundant javadoc tags

* mark takeState() final to prevent subclass breaker accounting bypass

* add multi-bucket success and PercentileRanks circuit breaker tests

* revert PercentileRanks breaker tests: fix is in the abstract class, already covered

* remove duplicate multi-bucket success breaker test

* remove redundant comments above breaker assertions

* update comments, explain more clearly

* address review feedback, add more clear comments.

* make withSequentialIndex method static

* Mute LookupJoinExpression CurrentCoordinator BWC tests

* Revert LookupJoinExpression CurrentCoordinator BWC test mutes

---------

Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
@hawkhuang-collab hawkhuang-collab added :Analytics/ES|QL AKA ESQL >bug auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) backport Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) labels Aug 15, 2026
@elasticsearchmachine
elasticsearchmachine merged commit 14f96ce into elastic:9.5 Aug 15, 2026
39 checks passed
@hawkhuang-collab
hawkhuang-collab deleted the backport/9.5/pr-155586 branch August 15, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) backport >bug Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.5.2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants