Skip to content

fix: computeIfAbsent should not update write timestamp for existing entries#8301

Open
daguimu wants to merge 1 commit intogoogle:masterfrom
daguimu:fix/computeIfAbsent-write-timestamp-3700
Open

fix: computeIfAbsent should not update write timestamp for existing entries#8301
daguimu wants to merge 1 commit intogoogle:masterfrom
daguimu:fix/computeIfAbsent-write-timestamp-3700

Conversation

@daguimu
Copy link
Copy Markdown

@daguimu daguimu commented Mar 27, 2026

Problem

LocalCache.computeIfAbsent() incorrectly resets the write timestamp when called on an existing key, even though the value is not changed. This causes expireAfterWrite caches to behave like expireAfterAccess caches — entries never expire as long as they are accessed via computeIfAbsent() at intervals shorter than the write expiry duration.

This is particularly problematic for Spring Boot applications using @Cacheable with synchronized access, which delegates to computeIfAbsent() internally.

Root Cause

computeIfAbsent() delegates to Segment.compute(). When the computed value is the same reference as the existing value (newValue == valueReference.get()), the code path at LocalCache.java:2278 called recordWrite(e, 0, now), which resets the write timestamp via entry.setWriteTime(now). This incorrectly extends the expireAfterWrite deadline.

Fix

Replaced the recordWrite(e, 0, now) call with inline logic that:

  • Updates the access time (for expireAfterAccess correctness)
  • Re-adds the entry to both accessQueue and writeQueue (required because they were removed earlier in the compute() method at lines 2251-2252)
  • Does NOT reset the write timestamp, preserving the original expireAfterWrite deadline

Tests Added

Change Point Test
Do not reset write time when value unchanged testComputeIfAbsent_existingEntryWriteTimeNotChanged() — verifies getWriteTime() is unchanged after computeIfAbsent on existing key
Entry properly expires despite repeated computeIfAbsent testComputeIfAbsent_existingEntryExpiresAfterWrite() — verifies entry expires after write duration even with repeated computeIfAbsent calls
Regression: compute() with new value still updates write time testCompute_returningNewValueUpdatesWriteTime() — verifies that compute() returning a new value still correctly updates write time

Impact

Only affects the code path in Segment.compute() where the computed value is the same reference as the existing value. This path is exercised by computeIfAbsent() on existing keys, and also by compute()/computeIfPresent()/merge() when their function returns the existing value unchanged. The new-value and removal paths are unaffected.

Fixes #3700

…ntries

When computeIfAbsent is called on an existing key, the value is not
changed, but recordWrite was called which incorrectly reset the write
timestamp. This caused expireAfterWrite caches to behave like
expireAfterAccess, as repeated computeIfAbsent calls would extend the
entry's lifetime indefinitely.

The fix avoids calling recordWrite when the computed value is the same
reference as the existing value. Instead, it updates only the access
time and re-adds the entry to both access and write queues without
resetting the write timestamp.

Fixes google#3700
@chaoren chaoren added type=defect Bug, not working as expected status=triaged package=cache P4 no SLO labels Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P4 no SLO package=cache status=triaged type=defect Bug, not working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LocalCache computeIfAbsent updates write timestamp on existing entries, affecting expireAfterWrite caches

2 participants