Skip to content

fix: make LRU2Cache.computeIfAbsent thread-safe#16170

Open
daguimu wants to merge 1 commit intoapache:3.3from
daguimu:fix/lru2cache-computeifabsent-race-condition
Open

fix: make LRU2Cache.computeIfAbsent thread-safe#16170
daguimu wants to merge 1 commit intoapache:3.3from
daguimu:fix/lru2cache-computeifabsent-race-condition

Conversation

@daguimu
Copy link
Copy Markdown

@daguimu daguimu commented Mar 25, 2026

Problem

LRU2Cache.computeIfAbsent() releases the lock between get() and put(), breaking the atomicity that all other methods in this class maintain. Every other method (get, put, remove, size, clear, containsKey, setMaxCapacity) correctly holds the lock for the entire operation, but computeIfAbsent does not:

// Current broken code - lock released between get() and put()
public V computeIfAbsent(K key, Function<? super K, ? extends V> fn) {
    V value = get(key);       // acquires lock, releases lock
    if (value == null) {
        value = fn.apply(key); // no lock held
        put(key, value);       // acquires lock, releases lock
    }
    return value;
}

This is called from the Triple protocol hot path in StreamUtils.lruHeaderMap.computeIfAbsent(key, k -> k.toLowerCase()) for every RPC request.

Impact

Under concurrency, multiple threads can:

  1. Both see get(key) == null
  2. Both compute fn.apply(key) (duplicate work)
  3. Both call put(key, value) — the double-put defeats the LRU-2 promotion logic (first put goes to pre-cache, second put unintentionally promotes to main cache)

Root Cause

The method was composed from the public get() and put() methods (which each independently acquire/release the lock) instead of operating on the underlying LinkedHashMap directly under a single lock span.

Fix

Hold the lock across the entire computeIfAbsent operation, using super.get() / super.put() directly and inlining the LRU-2 promotion logic to avoid nested lock acquisition:

public V computeIfAbsent(K key, Function<? super K, ? extends V> fn) {
    lock.lock();
    try {
        V value = super.get(key);
        if (value == null) {
            value = fn.apply(key);
            if (preCache.containsKey(key)) {
                preCache.remove(key);
                super.put(key, value);
            } else {
                preCache.put(key, true);
            }
        }
        return value;
    } finally {
        lock.unlock();
    }
}

Tests Added

  • testComputeIfAbsentBasic — Verifies correct LRU-2 semantics: first call goes to pre-cache, second promotes, third returns from main cache without calling the function
  • testComputeIfAbsentConcurrency — 10 threads × 1000 iterations calling computeIfAbsent concurrently, verifying no errors or corruption

All 6 tests in LRU2CacheTest pass.

Impact

  • Minimal change: 1 method rewritten in 1 file
  • Preserves existing LRU-2 semantics
  • Fixes thread safety in Triple protocol header processing hot path

…s entire operation

LRU2Cache.computeIfAbsent() released the lock between get() and put(),
breaking the atomicity that all other methods in this class maintain.
This caused duplicate function computation under concurrency and could
defeat the LRU-2 promotion logic.

The fix holds the lock across the entire computeIfAbsent operation,
inlining the LRU-2 promotion logic to avoid nested lock acquisition.
@daguimu daguimu force-pushed the fix/lru2cache-computeifabsent-race-condition branch from abd1304 to 7a5fa6b Compare March 25, 2026 12:11
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.81%. Comparing base (63fe8c3) to head (7a5fa6b).
⚠️ Report is 1 commits behind head on 3.3.

Additional details and impacted files
@@             Coverage Diff              @@
##                3.3   #16170      +/-   ##
============================================
- Coverage     60.81%   60.81%   -0.01%     
+ Complexity    11765    11750      -15     
============================================
  Files          1953     1953              
  Lines         89118    89123       +5     
  Branches      13444    13445       +1     
============================================
+ Hits          54197    54200       +3     
+ Misses        29364    29361       -3     
- Partials       5557     5562       +5     
Flag Coverage Δ
integration-tests-java21 32.14% <100.00%> (-0.03%) ⬇️
integration-tests-java8 32.21% <100.00%> (-0.11%) ⬇️
samples-tests-java21 32.11% <100.00%> (-0.08%) ⬇️
samples-tests-java8 29.85% <100.00%> (+0.09%) ⬆️
unit-tests-java11 59.03% <100.00%> (-0.01%) ⬇️
unit-tests-java17 58.50% <100.00%> (-0.03%) ⬇️
unit-tests-java21 58.54% <100.00%> (+0.01%) ⬆️
unit-tests-java25 58.50% <100.00%> (-0.03%) ⬇️
unit-tests-java8 59.06% <100.00%> (+0.02%) ⬆️

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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants