Add cancellation checkpoints in field data loading and aggregation paths#21318
Add cancellation checkpoints in field data loading and aggregation paths#21318kaushalmahi12 wants to merge 1 commit intoopensearch-project:mainfrom
Conversation
0aed648 to
0a67de9
Compare
PR Reviewer Guide 🔍(Review updated until commit b68ca57)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to b68ca57 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 6f5e248
Suggestions up to commit 0a67de9
|
|
❌ Gradle check result for 0a67de9: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
0a67de9 to
6f5e248
Compare
|
Persistent review updated to latest commit 6f5e248 |
Cancelled search tasks continue consuming CPU indefinitely because key code paths in field data loading and global ordinals building have zero cancellation checkpoints. This causes zombie threads stuck in OrdinalsBuilder.addDoc() for days/weeks. This change adds cancellation checks at three levels: 1. ExitablePostingsEnum in ExitableDirectoryReader - wraps PostingsEnum.nextDoc() with sampled cancellation checks (every 8191 calls), closing the gap where PagedBytesIndexFieldData.loadDirect() iterates postings without cancellation. 2. GlobalOrdinalsBuilder.build() - adds per-segment cancellation check via an overloaded method that accepts a Runnable, with backward-compatible default. 3. AggregatorFactories.createTopLevelAggregators() - checks SearchContext.isCancelled() before each aggregator factory create, so deeply nested aggregation trees can be interrupted between levels. All changes are backward compatible - no interface changes, no method signature changes on existing methods. Plugins and modules are unaffected. Signed-off-by: Kaushal Kumar <kshkmr@amazon.com> Signed-off-by: Kaushal Kumar <ravi.kaushal97@gmail.com>
6f5e248 to
b68ca57
Compare
|
Persistent review updated to latest commit b68ca57 |
|
❌ Gradle check result for b68ca57: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
@jainankitk , @bowenlan-amzn , @sgup432 Could you please help review this PR? |
Description
Cancelled search tasks continue consuming CPU indefinitely because key code paths in field data loading and global ordinals building have zero cancellation checkpoints. This causes "zombie threads" — tasks cancelled but consuming 100% CPU for days/weeks.
Root Cause
The call chain
AggregationPhase.preProcess → AggregatorFactories.createTopLevelAggregators → TermsAggregatorFactory → GlobalOrdinalsBuilder.build → AbstractIndexOrdinalsFieldData.loadGlobalDirect → PagedBytesIndexFieldData.loadDirect → OrdinalsBuilder.addDoc → GrowableWriter.get/sethas no cancellation checkpoints.While
ExitableDirectoryReaderalready wrapsTermsEnum.next()with cancellation checks, it does not wrapPostingsEnum. The inner loop inPagedBytesIndexFieldData.loadDirect()iteratesdocsEnum.nextDoc()→OrdinalsBuilder.addDoc()in a tight CPU loop with no way to interrupt it once a task is cancelled.Changes
This PR adds cancellation checks at three levels:
1.
ExitablePostingsEnuminExitableDirectoryReader(the critical fix)ExitablePostingsEnumwrapsPostingsEnum.nextDoc()andadvance()with sampled cancellation checks (every 8191 calls, matching the existingExitableIntersectVisitorpattern)ExitableTermsEnum.postings()so any code path that iterates postings through the exitableReader gets coverage automaticallydocsEnum.nextDoc()→OrdinalsBuilder.addDoc()→GrowableWriter.get/setFrequencyFilterandRamAccountingTermsEnumbecauseFilteredTermsEnum.postings()delegates to the underlyingtenum2.
GlobalOrdinalsBuilder.build()— per-segment cancellation checkbuild()accepts aRunnable cancellationCheck, called between segment iterations() -> {}(no-op) — fully backward compatibleAbstractIndexOrdinalsFieldData.loadGlobalDirect(DirectoryReader, Runnable)overload threads the check through3.
AggregatorFactories.createTopLevelAggregators()— per-factory cancellation checksearchContext.isCancelled()before eachfactories[i].create()callWhy
PostingsEnumis the only Lucene construct that needed wrappingOther
LeafReaderiteration constructs (NumericDocValues,SortedSetDocValues, etc.) are used during aggregation collection, which is driven by the query scorer that already has cancellation checks.PostingsEnumis the only one used in an unbounded tight loop (loadDirect) that runs outside the query scorer's cancellation-aware iteration.Backward Compatibility
IndexFieldData,IndexOrdinalsFieldData,IndexFieldDataCache)IndexFieldDatadon't need any changeslowLevelCancellationis disabled,ExitableDirectoryReaderdoesn't wrap the reader, so there is zero overheadCache Safety
IndicesFieldDataCacheusescomputeIfAbsent— ifloadDirect()orloadGlobalDirect()throwsTaskCancelledException, the entry is simply not cached. No partial/corrupt cache entries.Testing
SearchCancellationTests.testExitablePostingsEnum— verifiesExitablePostingsEnumthrowsTaskCancelledExceptiononnextDoc()andadvance()when cancelledSearchCancellationTests.testExitablePostingsEnumNoOpWhenCancellationDisabled— verifies no wrapping overhead when cancellation is disabledGlobalOrdinalsBuilderTests— verifies per-segment cancellation check fires, delayed cancellation works, and original method backward compatAggregatorFactoriesCancellationTests— verifiescreateTopLevelAggregatorsthrows whenisCancelled()is trueCheck List
--signoffBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and Signing Commits, please see the Contribution Guidelines.