-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Fix EvictableCacheBuilder#expireAfterWrite behavior
#14476
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,7 +77,7 @@ class EvictableCache<K, V> | |
| tokens.remove(token.getKey(), token); | ||
| } | ||
| }), | ||
| new TokenCacheLoader<>(cacheLoader)); | ||
| new TokenCacheLoader<>(cacheLoader, tokens)); | ||
| } | ||
|
|
||
| @SuppressModernizer // CacheBuilder.build(CacheLoader) is forbidden, advising to use this class as a safety-adding wrapper. | ||
|
|
@@ -104,8 +104,13 @@ public V get(K key, Callable<? extends V> valueLoader) | |
| { | ||
| Token<K> newToken = new Token<>(key); | ||
| Token<K> token = tokens.computeIfAbsent(key, ignored -> newToken); | ||
| Callable<? extends V> valueLoaderImpl = () -> { | ||
| // revive token if it got expired before reloading | ||
| tokens.computeIfAbsent(token.getKey(), ignored -> token); | ||
| return valueLoader.call(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just thinking -- should the revival happen before or after
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. related -- if |
||
| }; | ||
| try { | ||
| return dataCache.get(token, valueLoader); | ||
| return dataCache.get(token, valueLoaderImpl); | ||
| } | ||
| catch (Throwable e) { | ||
| if (newToken == token) { | ||
|
|
@@ -394,16 +399,20 @@ private static class TokenCacheLoader<K, V> | |
| extends CacheLoader<Token<K>, V> | ||
| { | ||
| private final CacheLoader<? super K, V> delegate; | ||
| private final ConcurrentHashMap<K, Token<K>> tokens; | ||
|
|
||
| public TokenCacheLoader(CacheLoader<? super K, V> delegate) | ||
| public TokenCacheLoader(CacheLoader<? super K, V> delegate, ConcurrentHashMap<K, Token<K>> tokens) | ||
| { | ||
| this.delegate = requireNonNull(delegate, "delegate is null"); | ||
| this.tokens = requireNonNull(tokens, "tokens is null"); | ||
| } | ||
|
|
||
| @Override | ||
| public V load(Token<K> token) | ||
| throws Exception | ||
| { | ||
| // revive token if it got expired before reloading | ||
| tokens.computeIfAbsent(token.getKey(), ignored -> token); | ||
| return delegate.load(token.getKey()); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| import org.testng.annotations.DataProvider; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
@@ -43,8 +44,10 @@ | |
| import static io.trino.collect.cache.CacheStatsAssertions.assertCacheStats; | ||
| import static java.lang.Math.toIntExact; | ||
| import static java.lang.String.format; | ||
| import static java.time.temporal.ChronoUnit.MILLIS; | ||
| import static java.util.concurrent.Executors.newFixedThreadPool; | ||
| import static java.util.concurrent.TimeUnit.DAYS; | ||
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
@@ -108,6 +111,26 @@ public void testEvictByWeight() | |
| assertThat(cache.asMap().values().stream().mapToInt(String::length).sum()).as("values length sum").isLessThanOrEqualTo(20); | ||
| } | ||
|
|
||
| @Test(timeOut = TEST_TIMEOUT_MILLIS) | ||
| public void testLoadOnceWithTimeEviction() | ||
| throws Exception | ||
| { | ||
| int ttl = 50; | ||
| int expectedCalls = 10; | ||
|
|
||
| AtomicInteger counter = new AtomicInteger(); | ||
| Cache<String, Integer> cache = EvictableCacheBuilder.newBuilder() | ||
| .expireAfterWrite(ttl, MILLISECONDS) | ||
| .build(); | ||
|
|
||
| Instant until = Instant.now().plus(ttl * expectedCalls, MILLIS); | ||
| while (until.isAfter(Instant.now())) { | ||
|
Comment on lines
126
to
127
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Stopwatch -- it's ligheter-weight and easier to lead.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW i wanted to put a PR with a test preventing memory leak (#14476 (comment)) and end-up also writing my version of this test #14502. not sure if this helps |
||
| cache.get("foo", counter::incrementAndGet); | ||
| Thread.sleep(1); | ||
| } | ||
| assertThat(counter.get()).isEqualTo(expectedCalls); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the timeout
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It never goes past the threshold of the last expiration. Or am I missing something?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't know how much time elapses between checking the clock and invoking
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. am i missing something?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this boils down to the fact it's much better to use ~Ticker for tests, per @losipiuk #14502 (comment) |
||
| } | ||
|
|
||
| @Test(timeOut = TEST_TIMEOUT_MILLIS) | ||
| public void testReplace() | ||
| throws Exception | ||
|
|
||
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.
would this be equivalent?