[ 
https://issues.apache.org/jira/browse/SOLR-18506?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Nick Shanin updated SOLR-18506:
-------------------------------
    Issue Type: Test  (was: Bug)

> TestThinCache.testSimple is flaky: a warming-phase eviction can hit the 
> second cache's own scope
> ------------------------------------------------------------------------------------------------
>
>                 Key: SOLR-18506
>                 URL: https://issues.apache.org/jira/browse/SOLR-18506
>             Project: Solr
>          Issue Type: Test
>            Reporter: Nick Shanin
>            Priority: Minor
>              Labels: pull-request-available
>          Time Spent: 40m
>  Remaining Estimate: 0h
>
> #AI-generated, human-approved text below.
> Summary
> org.apache.solr.search.TestThinCache.testSimple fails intermittently with
> "AssertionError: expected:<0> but was:<1>" at TestThinCache.java:170, 
> together with a
> classMethod teardown error reporting 3 unreleased SolrMetricsContext objects. 
> The test is the
> only coverage of ThinCache, the per-searcher scope over a shared node-level 
> cache introduced
> in SOLR-16654. The failure has been recorded 14 times in the last 30 days 
> across unrelated
> pull requests (list below), including 10 times on branch_9x. It is a test 
> defect, not a
> product defect: no production code is involved.
> Failure signature
> - TestThinCache.testSimple: expected:<0> but was:<1> at 
> TestThinCache.java:170 (the second
>   cache's evictions metric). The value is exactly 1 on every recorded 
> occurrence.
> - classMethod teardown: 3 unreleased SolrMetricsContext objects (knock-on, 
> see root cause).
> - Recorded occurrences (GitHub "Solr Tests via Crave" runs, 2026-09-09 to 
> 2026-10-04):
>   PR #4894 (seed C4077EAB80AE9E50), PR #4939 (60B12BB09380AC51), PR #4943 
> (E58B5E1FFDEBA340),
>   PR #5013 (30A1B1A830109349), and PR #4976 on branch_9x ten times 
> (E4DD10799981D14B,
>   2E53727468DE4DF1, 391A297131D7D9C1, D8FD97BF9A8A48B0, FCA68807697335BA, 
> 861F48CD4DFF5F51,
>   D5B22838079384ED, 6E0A7AD71C551639, 7A1C354A4C7E10DC, FA416BA281EBD0DF).
> Reproduction
> Locally, the failure does not reproduce with the recorded seeds: all 14 
> recorded seeds were
> run against current main with forced re-execution (cleanTest), plus 
> default-seed runs, and
> every run passed. That is expected under the verified mechanism below: the 
> outcome does not
> depend on the test seed, so no seed can reproduce it. It reproduces 
> statistically instead.
> A standalone model of the test's put/get sequence against Caffeine 3.2.4 (the 
> version this
> tree pins), run with 200,000 trials per configuration, fails the evictions 
> assertion in
> 1,243 trials (0.62 percent) at the test's capacity of 100 and in none at the 
> fixed capacity
> of 200.
> Root cause
> Eviction counting in ThinCache is per scope. A ThinCache counts an eviction 
> only in
> onRemoval, dispatched through the backing cache's removal listener registry, 
> and a scope
> registers with that registry only in initForSearcher, called from 
> initialSearcher and warm.
> testSimple wires the first cache by hand and never triggers that registration 
> for it, so the
> first cache's eviction counter is always 0, and the priorEvictions that warm 
> copies into the
> second cache is always 0.
> The second cache registers at warm. warm performs its 25 warming puts first 
> and only then
> resets the eviction counter, so evictions counted during warming are erased; 
> the only
> operation that can leave a count afterwards is the test's put of key 103, and 
> one put evicts
> at most one entry. That is why the failure value is always exactly 1.
> Until the fix, warming and the put of key 103 ran against a backing cache 
> still at capacity
> 100 and already full, so each put evicted one entry. Which entry Caffeine 
> picks as the victim
> depends on key hashes. ScopedKey.hashCode is Objects.hash(scope, key) and the 
> test's scopes
> are plain new Object() instances, so the hashes come from identity hash 
> codes: they vary per
> JVM and do not follow the test seed. Usually the victim is a first-cache 
> entry, which is
> never counted. Occasionally (about 0.6 percent of runs in the model) the 
> victim is one of the
> second cache's own entries, the count becomes 1, and the assertion fails.
> There is no notification timing race. CaffeineCache defaults async to true 
> and then sets a
> same-thread executor (Runnable::run); the test's backing.init passes only 
> size and
> initialSize, so evictions and their removal notifications complete 
> synchronously inside the
> put that triggers them. In the model, the phase-1 eviction was already 
> delivered when the
> 101st put returned in 200,000 of 200,000 trials, no listener call ran off the 
> calling thread,
> and a later cleanUp() never delivered anything.
> The teardown leak is a knock-on: the failed assertion skips the test's 
> closing of its
> SolrMetricsContext, and the framework reports the context and its two 
> per-cache children as
> unreleased.
> Record of corrections: this ticket's original description located the race at 
> warm's copy of
> the first cache's counter and proposed expecting an inherited value of 1. 
> Building that fix
> falsified it (the first cache's counter reads 0 deterministically, because it 
> never
> registers). The replacement account then kept a delivery-timing race in the 
> second phase;
> review of the shipped PR falsified that too, by the synchronous-executor 
> evidence above and
> by the model. The mechanism stated here is the verified one.
> Fix
> Shipped in PR #5029 ([https://github.com/apache/solr/pull/5029]), test-only: 
> after the first
> cache's assertions, enlarge the shared backing cache to 200 through its 
> public setMaxSize.
> The test puts 127 keys in total and holds at most 126 at once, so at capacity 
> 200 the second
> phase cannot evict under any interleaving, and the eviction assertion keeps 
> its original
> expected value of 0. Proof is the structural argument (capacity above the 
> held count), with
> the 200,000-trial model as consistency evidence (0.62 percent failures at 
> capacity 100, none
> at 200) and 32 forced full-class runs at the PR head as a regression check.
> One residual is deliberately left: assertNull(lfuCache.get(1)) in the first 
> phase depends on
> the same victim choice (the phase-1 eviction occasionally picks a victim 
> other than key 1)
> and can still fail very rarely, about 1 run in 10,000 in the model, with or 
> without this fix.
> This PR does not address it, and the test is not claimed to be deterministic.
> #AI assistance
> AI agents assisted with research, implementation, review, and drafting. Nick 
> Shanin directed
> the work and takes responsibility for this contribution.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to