Use point-in-time key snapshot for fielddata cache cleanup - #22499
Conversation
PR Reviewer Guide 🔍(Review updated until commit 08ebb96)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 08ebb96 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit d38afcd
Suggestions up to commit 1ae96ef
Suggestions up to commit 73971a0
Suggestions up to commit f66fcee
|
|
❌ Gradle check result for f66fcee: null 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? |
sgup432
left a comment
There was a problem hiding this comment.
@jwils Thanks for the PR. Can you remove this from draft state if this is ready?
Also do we need the original PR now? As this PR solves all the leaks happening within field data cache and not just with the stale segment reader. I would prefer to merge this in.
|
This does not fix any issue. Let's fix and do patches the memory leak first. |
And can you explain me why? I see the original issue was that we are iterating over a live LRU list which can reshuffle and we might miss a entry during a certain pass. This completely avoids it. You have also a test you wrote apparently which you can run against this approach and verify. |
|
Tagging maintainers here for a second eye here - @andrross @reta @msfroh @jainankitk |
|
My frustration here is my AWS TAM is saying you don't believe the first PR fixes the memory leak. If you can confirm you just prefer the style of this PR vs the original PR I can adjust the style here. I'd like to be explicit that the first PR as written when opened shows and fixes a memory leak in Opensearch 3.3. |
f66fcee to
73971a0
Compare
|
Persistent review updated to latest commit 73971a0 |
|
@jwils This is an opensource community not just managed by AWS but by others as well. So lets keep the AWS specific conversation out of here. If you need help with AWS specific issues, better to engage via other internal channel. From my side, I agree this is a real issue. I'm just looking for the right solution. The earlier PR partially fixes it for one major case, whereas this PR covers all of them. And by exposing a keysSnapshot method, this change also lets anyone else reuse it if needed. |
@jwils @sgup432 's request for test cases is something that is reasonable and asked of every contributor to the project. If you need guidance for this, the community (including us) are always willing to help. |
|
My read is we now agree on the mechanism for a memory leak in opensearch. The live keys() iteration could skip entries when the LRU reshuffled mid-sweep, and on the reader-close path a miss was permanent. #22491 fixed that permanent leak with exact-key invalidation; #22499 fixes the same leak plus the per-index/per-field sweep misses by removing the unsafe iteration entirely, and is the approach we're merging and while the implementation may have been buggy for a while this issue was caused by #19116. The regression test fails on main and passes with either fix. @sgup432 is that a fair summary? |
73971a0 to
1ae96ef
Compare
|
Persistent review updated to latest commit 1ae96ef |
|
@kkhatua adding a test is not a problem. Done. |
|
You all of course get to make the decision here, but let me also give one more recommendation as to why I believe #22491 is the better branch to backport to 3.3 and a good independent fix. It fixes the memory leak by reverting the invalidation to the previous behavior that runs in O(1) and does not introduce new allocations or locking mechanisms. |
|
❌ Gradle check result for 1ae96ef: 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? |
|
Can't help myself, another datapoint after a day of running the other patch. Happy to deploy this change as well to compare, but looking at our last blue green deployment of 3.3 on June 17 you can see old gen jvm heap rose to about 60% of the heap. This rise was rapid and I doubt accounted for by the memory leak as it happened pretty quickly and we see the later rise is much slower. You can see one of those clear leaks occur right at the end of the graph. Note on this graph we were going from a 64GB heap i8g.4xlarge, so the heap usage before the B/G shouldn't be compared directly.
This graph shows the current deployment of #22491 which appears to be extremely stable at 45% old gen heap. Given we run with a custom larger memory instance I imagine this difference would be even more pronounced for those with high write load running with 32 GB or less of heap.
I'd be open to working with AWS to test this fix out as well so we can compare the old gen heap usage between the two patches. The difference in old gen usage is significantly more than I would have expected. |
|
Thanks @sgup432 — and apologies for getting a bit off track across these two PRs. I appreciate the careful review of the data here and will aim to hold my responses to the same standard. |
|
Another interesting datapoint, this time comparing steady-state fielddata size under production query load. East is running #22491; west is unpatched 3.3, sampled ~1 hour after a fielddata cache clear to hopefully minimize the contribution of already-leaked entries. Given our high cache turnover, you can see the west cache runs almost twice as large under the 1-min sweep interval — sawtoothing between ~70–100 MB while east holds ~52–64 MB. It's currently hard to track and not sure if there is a way, but I'd be interested in pinning down how much heap actually gets retained here. If it's really just ~50 MB of fielddata it's probably not a big issue, but if those entries pin substantially more than the reported memory_size, that could account for some of the old-gen difference in the graphs above.
|
|
I've merged #22491. My two cents (caveat I'm not really a deep expert on all this caching code):
In other words doing both PRs is probably worthwhile. |
1ae96ef to
d38afcd
Compare
|
Persistent review updated to latest commit d38afcd |
|
Thank you. I agree with that approach. I've rebased this pr and switched back to that method. |
|
Tagging @jainankitk for final review and merge after @sgup432 's review |
sgup432
left a comment
There was a problem hiding this comment.
Overall LGTM, just one minor comment
The fielddata cache cleanup sweep iterated Cache.keys(), which walks the LRU linked list without holding the LRU lock; a concurrent cache hit relinking the current entry to the head can silently skip entries, so per-index/per-field clears could miss stale entries and retain them indefinitely. Add Cache.keysSnapshot(), a weakly consistent point-in-time copy taken one segment at a time under each segment's read lock (never blocking cache reads and never holding a global lock for O(n) work), and sweep that snapshot with exact-key invalidation instead. Reader-close cleanup already invalidates its exact key synchronously (opensearch-project#22491) and does not rely on the sweep. Signed-off-by: Josh Wilson <joshuaw@squareup.com>
d38afcd to
08ebb96
Compare
|
Persistent review updated to latest commit 08ebb96 |
|
Done and rebased. |
|
❕ Gradle check result for 08ebb96: UNSTABLE Please review all flaky tests that succeeded after retry and create an issue if one does not already exist to track the flaky failure. |



Description
The fielddata cache cleanup sweep iterated Cache.keys(), which walks
the LRU linked list without holding the LRU lock; a concurrent cache
hit relinking the current entry to the head can silently skip entries,
so per-index/per-field clears could miss stale entries and retain them
indefinitely. Add Cache.keysSnapshot(), a weakly consistent
point-in-time copy taken one segment at a time under each segment's
read lock (never blocking cache reads and never holding a global lock
for O(n) work), and sweep that snapshot with exact-key invalidation
instead. Reader-close cleanup already invalidates its exact key
synchronously (#22491) and does not rely on the sweep.
Verification
Leak reproduction through the real
clear()path. Harness: node-level cache with 505k entries (5k belonging to a target index), 4 threads hammeringcache.get()across all entries to simulate search traffic, 30 rounds ofclear(targetIndex)+ sweep, counting target entries that survive. Same harness binary run against each build:Iterator coverage under concurrent promotion (1M-entry
Cache, 4 reader threads): the livekeys()walk missed keys in 200/200 iterations — the worst walk observed only 168,881 of 1,000,000 keys — whilekeysSnapshot()missed none in 200 iterations.Snapshot cost (quiesced cache): 1.6 ms at 100k keys, 10.7 ms at 1M keys, spread across per-segment read locks.
Search latency impact (single node, 30 indices x 999 fielddata fields ≈ 124k keys / 121 MB, 12 search threads, 1s cleanup interval, 25 clear-and-sweep rounds): parity with the base branch.
Related Issues
Supersedes #22491 (see review discussion there and in this PR).
Check List
By 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 off your commits, please check here.
Update (2026-07-29)
invalidateKey(Key)package-private per review and kept its removal-failure coverage in the same package without widening production visibility.