Skip to content
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

Merged
merged 33 commits into from
Oct 9, 2015
Merged

Replace Guava cache with simple concurrent LRU cache #13879

merged 33 commits into from
Oct 9, 2015

Conversation

jasontedor
Copy link
Member

This pull request replaces the Guava cache with a simple concurrent LRU cache with flexible eviction policies.

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;
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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()) {
Copy link
Contributor

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 ?

Copy link
Member Author

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.

@jasontedor jasontedor mentioned this pull request Oct 2, 2015
72 tasks
* 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
Copy link
Member

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/ ?

Copy link
Member Author

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()) {
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Member Author

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) {
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it in a556e31.

@jpountz
Copy link
Contributor

jpountz commented Oct 8, 2015

it looks good to me but I would like someone else (@nik9000 ?) to also do another round of review before we merge

@jasontedor
Copy link
Member Author

@nik9000 Would you be able to take another look?

@nik9000
Copy link
Member

nik9000 commented Oct 9, 2015

@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()) {
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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>> {
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

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

++

Copy link
Member Author

Choose a reason for hiding this comment

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

Added comment in 59c9049.

@nik9000
Copy link
Member

nik9000 commented Oct 9, 2015

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?

@jasontedor
Copy link
Member Author

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.

@nik9000 Did you have something in mind that is different than what is in CacheTests#testCacheStats? I think this tests both hit and miss stats, plus evictions.

Is it worth adding a stat for additions?

I don't think so (am I being silly in thinking that it's just Cache.count() + Cache.stats().getEvictions()?

@nik9000
Copy link
Member

nik9000 commented Oct 9, 2015

You are right on both counts there.

LGTM

@jasontedor
Copy link
Member Author

@nik9000 @jpountz @dakrone @rmuir Thanks for reviewing. Bringing it home!

jasontedor added a commit that referenced this pull request Oct 9, 2015
Replace Guava cache with simple concurrent LRU cache
@jasontedor jasontedor merged commit 50368b3 into elastic:master Oct 9, 2015
@jasontedor jasontedor deleted the straight-cache-homey branch October 9, 2015 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants