Skip to content

Fix Broken Lucene Query Tracking and Cancellation for OOM Protection#17884

Merged
Jackie-Jiang merged 8 commits intoapache:masterfrom
anuragrai16:luceneOOMKillingImprovements
Apr 2, 2026
Merged

Fix Broken Lucene Query Tracking and Cancellation for OOM Protection#17884
Jackie-Jiang merged 8 commits intoapache:masterfrom
anuragrai16:luceneOOMKillingImprovements

Conversation

@anuragrai16
Copy link
Copy Markdown
Contributor

Prospective fix for #17877

Summary

Fix missing CPU/memory tracking for realtime Lucene and HNSW index searcher threads
Enable OOM killer to properly terminate TEXT_MATCH and VECTOR_SIMILARITY queries

Changes

RealtimeLuceneTextIndex: Propagate QueryThreadContext to async searcher threads for resource tracking
MultiColumnRealtimeLuceneTextIndex: Same fix for multi-column text index variant
MutableVectorIndex: Convert synchronous Lucene search to async pattern using RealtimeLuceneTextIndexSearcherPool, preventing FSDirectory corruption on thread interrupt while enabling proper resource tracking
LuceneDocIdCollector/HnswDocIdCollector: Add periodic checkTerminationAndSampleUsagePeriodically() calls in collectors to detect OOM termination during document collection

Tests
RealtimeLuceneTextIndexResourceTrackingTest: Unit tests verifying context propagation, thread registration, and termination handling
TextMatchOomKillingIntegrationTest: Integration test validating OOM killing for TEXT_MATCH queries

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 14, 2026

Codecov Report

❌ Patch coverage is 28.57143% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.76%. Comparing base (9866ce4) to head (aed3474).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...ertedindex/MultiColumnRealtimeLuceneTextIndex.java 0.00% 37 Missing ⚠️
...me/impl/invertedindex/RealtimeLuceneTextIndex.java 43.33% 15 Missing and 2 partials ⚠️
...local/realtime/impl/vector/MutableVectorIndex.java 0.00% 15 Missing ⚠️
...pl/invertedindex/RealtimeLuceneDocIdCollector.java 66.66% 2 Missing ⚠️
...gment/index/readers/text/LuceneDocIdCollector.java 60.00% 2 Missing ⚠️
...gment/index/readers/vector/HnswDocIdCollector.java 60.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #17884      +/-   ##
============================================
+ Coverage     63.74%   63.76%   +0.01%     
  Complexity     1538     1538              
============================================
  Files          3160     3160              
  Lines        190755   190782      +27     
  Branches      29272    29272              
============================================
+ Hits         121595   121649      +54     
+ Misses        59612    59594      -18     
+ Partials       9548     9539       -9     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-11 63.73% <28.57%> (+0.02%) ⬆️
java-21 63.72% <28.57%> (+<0.01%) ⬆️
temurin 63.76% <28.57%> (+0.01%) ⬆️
unittests 63.76% <28.57%> (+0.01%) ⬆️
unittests1 56.25% <6.66%> (+<0.01%) ⬆️
unittests2 34.21% <28.57%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jackie-Jiang Jackie-Jiang added bugfix oom-protection Related to out-of-memory protection mechanisms labels Mar 18, 2026
Copy link
Copy Markdown
Contributor

@Jackie-Jiang Jackie-Jiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!

"Failed while releasing the searcher manager for realtime text index for columns {}, exception {}",
_columns, e.getMessage());
// Propagate context to register searcher thread for CPU/memory tracking
if (parentContext != null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryThreadContext.contextAwareExecutorService() provides a wrapper over the executor to maintain the context. You may modify RealtimeLuceneTextIndexSearcherPool to initialize a wrapped executor service instead

Copy link
Copy Markdown
Contributor Author

@anuragrai16 anuragrai16 Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with contextAwareExecutorService() is that it also registers the future with executionContext.addTask(future) so that it can be terminated with task.cancel(true);. For Lucene index searchers on consuming segments (that have an Index writer too), there is a problem in Lucene (see this), where it doesn't react well to Thread interruption and can corrupt the underlying FSDirectory used to do Index creation as well.

So, instead we only propagate the QueryThreadContext explicitly to make sure tracking of this works and not register the future to be cancelled. The cancellation is done manually in a cooperative way with _shouldCancel. This is only relevant for realtime segment lucene searches (that shares the same FSDirectory object b/w the IndexWriter and IndexSearcher).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. In that case, I'd suggest adding a boolean flag to control whether to skip registering task for auto termination into QueryThreadContext.contextAwareExecutorService(). Wrapping the logic in the same place is easier to track and manage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Updated.

import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

public class TextMatchOomKillingIntegrationTest extends BaseClusterIntegrationTest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long does this test run and how robust is it?

Copy link
Copy Markdown
Contributor Author

@anuragrai16 anuragrai16 Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It runs for 2-4 mins on my machine. OOM Killing in itself is indeterministic and will depend on the docker jvm that is used to run this test. So, I've made this test accept either outcome for now with,

if (exceptionsNode.isEmpty()) {
  // Query completed successfully - verify resource tracking worked
  assertTrue(memAllocated > 0 || cpuTime > 0, ...);
} else {
  // Query was killed - verify it was OOM killed
  verifyOomKill(query, response);
}

from function verifyOomKillOrResourceTracking()

so, the test in itself would not fail, and would demonstrate either the tracking worked and the query was not killed. But in most cases, would show the query being killed since the threshold for the heap is set to very low (15%) for query killing and 0% for ALARMING tracking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a 2-4 minutes test for this bug fix would be too much overhead. Do you see a way to make it light weight? E.g. a unit test to ensure the memory usage is tracked

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. I've already added a RealtimeLuceneTextIndexResourceTrackingTest.java‎ that should be enough for now, will evaluate an integration test and raise it separately if needed.

_numDocsCollected++, "RealtimeLuceneDocIdCollector");
} catch (RuntimeException e) {
// Convert to CollectionTerminatedException for clean Lucene handling
throw new CollectionTerminatedException();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a separate exception? We have special handling on TerminationException so it would be good to preserve it for correct error message in query response

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception is needed for Lucene IndexSearcher to correctly close all open objects. See documentation of this exception here.

Throw this exception in LeafCollector.collect(int) to prematurely terminate collection of the current leaf.
Note: IndexSearcher swallows this exception and never re-throws it.

Note that, the Lucene collector is run via the IndexSearcher, and the original interrupted TerminationException is preserved via RealtimeLuceneTextIndex/MultiColumnRealtimeLuceneTextIndex that catches the interruption and retains the correct error message that the TEXT_MATCH was interrupted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Could you please add some more comments explaining this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@xiangfu0 xiangfu0 added text-search Related to text/Lucene indexing and search bug Something is not working as expected index Related to indexing (general) real-time Related to realtime table ingestion and serving and removed bugfix labels Mar 20, 2026
Copy link
Copy Markdown
Contributor

@Jackie-Jiang Jackie-Jiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM otherwise. Nice work

Comment thread pinot-spi/src/main/java/org/apache/pinot/spi/query/QueryThreadContext.java Outdated
Comment thread pinot-spi/src/main/java/org/apache/pinot/spi/query/QueryThreadContext.java Outdated
@anuragrai16 anuragrai16 force-pushed the luceneOOMKillingImprovements branch from eae2f72 to aed3474 Compare April 1, 2026 13:47
@Jackie-Jiang Jackie-Jiang merged commit 6be0d94 into apache:master Apr 2, 2026
16 checks passed
@anuragrai16 anuragrai16 deleted the luceneOOMKillingImprovements branch April 2, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something is not working as expected index Related to indexing (general) oom-protection Related to out-of-memory protection mechanisms real-time Related to realtime table ingestion and serving text-search Related to text/Lucene indexing and search

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants