-
Notifications
You must be signed in to change notification settings - Fork 24.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace Guava cache with simple concurrent LRU cache #13879
Conversation
This commit adds a concurrent cache with flexible eviction policies. In particular, this cache supports: 1. concurrency 2. weight-based evictions 3. time-based evictions 4. manual invalidation 5. removal notification 6. cache statistics Closes #13717
This commit removes and now forbids all uses of com.google.common.cache.Cache, com.google.common.cache.CacheBuilder, com.google.common.cache.RemovalListener, com.google.common.cache.RemovalNotification, com.google.common.cache.Weigher across the codebase. This is a major step in the eventual removal of Guava as a dependency. Relates #13224
private long maximumWeight = -1; | ||
|
||
// the weigher of entries | ||
private ToLongBiFunction<K, V> weigher = (k, v) -> 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can it be a required argument of the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jpountz It can be, but I'm missing the advantage. Can you help me understand why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just trying to see how we could make some variables final so that the compiler ca help us. In addition this one variable looked to me like you would almost always want o use a custom weigher.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think this will have any noticeable impact on the optimizations that the compiler can find; I think this general discussion around final variables should come down to whether or not we want to have a single constructor (still package private) for setting all these optional fields, or use the setters. These fields are semantically final, it's just not being enforced currently because of the approach to use setters to build the cache from the builder.
This commit adds supports for expiration after writes to Cache. This enables entries to expire after they were initially placed in the cache without prolonging their life on retrieval. Replacements are considered new writes.
Integer key = randomIntBetween(1, numberOfEntries); | ||
cache.put(key, randomAsciiOfLength(10)); | ||
count.incrementAndGet(); | ||
if (rarely()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while i know some of these helper methods are friendly, when testing concurrency for something like this, that will be used everywhere, I would remove any other concurrency from the equation: otherwise you have happens-befores that could hide bugs.
All of the lines above cause synchronization: calls to random()
, either explicitly or implicitly via randomIntBetween
, randomAsciiOfLength
, rarely
etc synchronize on a global lock in RandomizedContext
.
The AtomicInteger
seems unnecessary, can we just change this assert at the end to compare against numberOfEntries * numberOfThreads
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very astute observation, thank you. I've addressed in 0999b7b7bf44a5889178898f32600e784046adbd.
* evictions are exposed. | ||
* <p> | ||
* The design of the cache is relatively simple. The cache is segmented into 256 segments which are backed by HashMaps. | ||
* The segments are protected by a re-entrant read/write lock. The read/write locks permit multiple concurrent readers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/The segments are/Each segment is/ ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Addressed in 0fb9081.
V value = get(key, now); | ||
if (value == null) { | ||
CacheSegment<K, V> segment = getCacheSegment(key); | ||
try (ReleasableLock ignored = segment.writeLock.acquire()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this lock is only used to ensure that we don't compute the same entry twice at the same time, so I don't think that we actually need to use the segment write lock, which has the downside to block reads on this segment. Should we use a second set of locks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jpountz Imagine simultaneous calls to put
and computeIfAbsent
for the same key. It seems to me the best synchronization mechanism between these two is the segment lock. Note that it's okay if the put
overwrites the result of the computeIfAbsent
, but we don't want computeIfAbsent
to not observe that put
is placing a value for the same key (lest we needlessly invoke the loader). Do you still think we should use a different synchronization mechanism?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok let's avoid the concurrent put/computeIfAbsent issue for now, we can try to improve in the future if we observe slow concurrent access
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so maybe let's just leave a comment about what you said?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Done in a6abb4f.
final Entry<K, V> before = entry.before; | ||
final Entry<K, V> after = entry.after; | ||
|
||
if (before == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add an assert entry == head
for sanity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it in a556e31.
it looks good to me but I would like someone else (@nik9000 ?) to also do another round of review before we merge |
@nik9000 Would you be able to take another look? |
Sure! I'll have a look soon. |
|
||
private boolean promote(Entry<K, V> entry, long now) { | ||
boolean promoted = true; | ||
try (ReleasableLock ignored = lruLock.acquire()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need to acquire the lock at all for DELETED
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nik9000 Without this lock it could be the case that entry.state
is not State.DELETED
, then we enter the lock, and now entry.state
is State.DELETED
. The check needs to happen after a synchronization barrier prevents any mutations to the entry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Maybe a comment for that?
}; | ||
} | ||
|
||
private class CacheIterator implements Iterator<Entry<K, V>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit rusty on my concurrency stuff, but I think there are cases where this will iterate over the same keys twice sometimes or skip some keys. Like if next gets thrown back to the head of the LRU then you start iteration over. Or if a key gets pushed in front of then you skip it. I think we're ok with these, or, rather, we have to be ok with these but its worth documenting if they are indeed possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Iteration over the keys was never intended to be thread-safe but only best effort. I agree that we should certainly document this, and I don't think an effort should be made to make it thread-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added comment in 59c9049.
Anything else I do will be knit picking. LGTM. Lets get it in and kick the tires. I think its worth adding a test for the hit and miss stats. I scanned for one and didn't see it but I could have just missed it. Is it worth adding a stat for additions? |
@nik9000 Did you have something in mind that is different than what is in
I don't think so (am I being silly in thinking that it's just |
You are right on both counts there. LGTM |
Replace Guava cache with simple concurrent LRU cache
This pull request replaces the Guava cache with a simple concurrent LRU cache with flexible eviction policies.