Visitar URL original
Fix computeIfAbsent resetting expireAfterWrite for existing entries by Sanjays2402 · Pull Request #8828 · google/guava · GitHub
Skip to content

Fix computeIfAbsent resetting expireAfterWrite for existing entries - #8828

Open
Sanjays2402 wants to merge 1 commit into
google:masterfrom
Sanjays2402:fix-3700-computeifabsent-write-time
Open

Sanjays2402 wants to merge 1 commit into
google:masterfrom
Sanjays2402:fix-3700-computeifabsent-write-time

Conversation

@Sanjays2402

Copy link
Copy Markdown

Fixes #3700

Calling cache.asMap().computeIfAbsent() for a key that is already present
reset the entry's write timestamp, so with expireAfterWrite the entry
effectively never expired as long as computeIfAbsent kept being called.
That contradicts the Map contract for computeIfAbsent, which leaves an
existing mapping untouched, and it made an expireAfterWrite cache behave
like an expireAfterAccess one for those keys.

The cause is in Segment.compute: when the remapping function returns the
very same value that is already stored, the code restored the old value
reference but still recorded a write, which updates the write timestamp.
This change records a read instead, which refreshes the access time but
leaves the write time alone, and re-adds the entry to the write queue so
it can still expire. The same branch covers compute, computeIfPresent
and merge returning the unchanged value, which get the same treatment.

Repro, before the fix (a manual Ticker, expireAfterWrite of 10
minutes): put at t=0, computeIfAbsent on the existing key at t=9,
then getIfPresent at t=11 returned the value. After the fix it returns
null. A second check confirms computeIfAbsent still refreshes the
access time for expireAfterAccess caches.

Tests: added testComputeIfAbsentDoesNotRefreshWriteTime and
testComputeIfAbsentStillRefreshesAccessTime to LocalCacheMapComputeTest.
Ran LocalCacheMapComputeTest, CacheExpirationTest, LocalCacheTest
and CacheEvictionTest: 10,370 tests, all passing.

@agammann agammann left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-adding e at the tail of writeQueue while retaining its old write time breaks the queue's timestamp order. expireEntries() only removes expired entries from the head, so an older entry can sit behind a newer, unexpired one. For example, with concurrencyLevel(1) and expireAfterWrite(10 minutes): put A at t=0, B at t=5, then computeIfAbsent(A, ...) at t=6. At t=11, cleanUp() sees B at the head and leaves expired A counted in size(). Could this preserve A's write-queue position, and add a two-entry FakeTicker test for cleanup order?

This branch has not been deployed

No deployments
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.

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

2 participants