| /* |
| * Copyright (C) 2011 The Guava Authors |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
| * in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| package com.google.common.cache; |
| |
| import static com.google.common.cache.TestingCacheLoaders.identityLoader; |
| import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener; |
| import static java.util.Arrays.asList; |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; |
| import static org.truth0.Truth.ASSERT; |
| |
| import com.google.common.cache.TestingCacheLoaders.IdentityLoader; |
| import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener; |
| import com.google.common.collect.Iterators; |
| import com.google.common.testing.FakeTicker; |
| import com.google.common.util.concurrent.Callables; |
| |
| import junit.framework.TestCase; |
| |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * Tests relating to cache expiration: make sure entries expire at the right times, make sure |
| * expired entries don't show up, etc. |
| * |
| * @author mike nonemacher |
| */ |
| @SuppressWarnings("deprecation") // tests of deprecated method |
| public class CacheExpirationTest extends TestCase { |
| |
| private static final long EXPIRING_TIME = 1000; |
| private static final int VALUE_PREFIX = 12345; |
| private static final String KEY_PREFIX = "key prefix:"; |
| |
| public void testExpiration_expireAfterWrite() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| checkExpiration(cache, loader, ticker, removalListener); |
| } |
| |
| public void testExpiration_expireAfterAccess() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| checkExpiration(cache, loader, ticker, removalListener); |
| } |
| |
| private void checkExpiration(LoadingCache<String, Integer> cache, WatchedCreatorLoader loader, |
| FakeTicker ticker, CountingRemovalListener<String, Integer> removalListener) { |
| |
| for (int i = 0; i < 10; i++) { |
| assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); |
| } |
| |
| for (int i = 0; i < 10; i++) { |
| loader.reset(); |
| assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); |
| assertFalse("Creator should not have been called @#" + i, loader.wasCalled()); |
| } |
| |
| CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); |
| |
| assertEquals("Map must be empty by now", 0, cache.size()); |
| assertEquals("Eviction notifications must be received", 10, |
| removalListener.getCount()); |
| |
| CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); |
| // ensure that no new notifications are sent |
| assertEquals("Eviction notifications must be received", 10, |
| removalListener.getCount()); |
| } |
| |
| public void testExpiringGet_expireAfterWrite() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| runExpirationTest(cache, loader, ticker, removalListener); |
| } |
| |
| public void testExpiringGet_expireAfterAccess() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| runExpirationTest(cache, loader, ticker, removalListener); |
| } |
| |
| private void runExpirationTest(LoadingCache<String, Integer> cache, WatchedCreatorLoader loader, |
| FakeTicker ticker, CountingRemovalListener<String, Integer> removalListener) { |
| |
| for (int i = 0; i < 10; i++) { |
| assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); |
| } |
| |
| for (int i = 0; i < 10; i++) { |
| loader.reset(); |
| assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); |
| assertFalse("Loader should NOT have been called @#" + i, loader.wasCalled()); |
| } |
| |
| // wait for entries to expire, but don't call expireEntries |
| ticker.advance(EXPIRING_TIME * 10, MILLISECONDS); |
| |
| // add a single unexpired entry |
| cache.getUnchecked(KEY_PREFIX + 11); |
| |
| // collections views shouldn't expose expired entries |
| assertEquals(1, Iterators.size(cache.asMap().entrySet().iterator())); |
| assertEquals(1, Iterators.size(cache.asMap().keySet().iterator())); |
| assertEquals(1, Iterators.size(cache.asMap().values().iterator())); |
| |
| CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); |
| |
| for (int i = 0; i < 11; i++) { |
| assertFalse(cache.asMap().containsKey(KEY_PREFIX + i)); |
| } |
| assertEquals(11, removalListener.getCount()); |
| |
| for (int i = 0; i < 10; i++) { |
| assertFalse(cache.asMap().containsKey(KEY_PREFIX + i)); |
| loader.reset(); |
| assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); |
| assertTrue("Creator should have been called @#" + i, loader.wasCalled()); |
| } |
| |
| // expire new values we just created |
| CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); |
| assertEquals("Eviction notifications must be received", 21, |
| removalListener.getCount()); |
| |
| CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); |
| // ensure that no new notifications are sent |
| assertEquals("Eviction notifications must be received", 21, |
| removalListener.getCount()); |
| } |
| |
| public void testRemovalListener_expireAfterWrite() { |
| FakeTicker ticker = new FakeTicker(); |
| final AtomicInteger evictionCount = new AtomicInteger(); |
| final AtomicInteger applyCount = new AtomicInteger(); |
| final AtomicInteger totalSum = new AtomicInteger(); |
| |
| RemovalListener<Integer, AtomicInteger> removalListener = |
| new RemovalListener<Integer, AtomicInteger>() { |
| @Override |
| public void onRemoval(RemovalNotification<Integer, AtomicInteger> notification) { |
| if (notification.wasEvicted()) { |
| evictionCount.incrementAndGet(); |
| totalSum.addAndGet(notification.getValue().get()); |
| } |
| } |
| }; |
| |
| CacheLoader<Integer, AtomicInteger> loader = new CacheLoader<Integer, AtomicInteger>() { |
| @Override public AtomicInteger load(Integer key) { |
| applyCount.incrementAndGet(); |
| return new AtomicInteger(); |
| } |
| }; |
| |
| LoadingCache<Integer, AtomicInteger> cache = CacheBuilder.newBuilder() |
| .removalListener(removalListener) |
| .expireAfterWrite(10, MILLISECONDS) |
| .ticker(ticker) |
| .build(loader); |
| |
| // Increment 100 times |
| for (int i = 0; i < 100; ++i) { |
| cache.getUnchecked(10).incrementAndGet(); |
| ticker.advance(1, MILLISECONDS); |
| } |
| |
| assertEquals(evictionCount.get() + 1, applyCount.get()); |
| int remaining = cache.getUnchecked(10).get(); |
| assertEquals(100, totalSum.get() + remaining); |
| } |
| |
| public void testRemovalScheduler_expireAfterWrite() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME); |
| } |
| |
| public void testRemovalScheduler_expireAfterAccess() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME); |
| } |
| |
| public void testRemovalScheduler_expireAfterBoth() { |
| FakeTicker ticker = new FakeTicker(); |
| CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); |
| WatchedCreatorLoader loader = new WatchedCreatorLoader(); |
| LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() |
| .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) |
| .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) |
| .removalListener(removalListener) |
| .ticker(ticker) |
| .build(loader); |
| runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME); |
| } |
| |
| public void testExpirationOrder_access() { |
| // test lru within a single segment |
| FakeTicker ticker = new FakeTicker(); |
| IdentityLoader<Integer> loader = identityLoader(); |
| LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder() |
| .concurrencyLevel(1) |
| .expireAfterAccess(11, MILLISECONDS) |
| .ticker(ticker) |
| .build(loader); |
| for (int i = 0; i < 10; i++) { |
| cache.getUnchecked(i); |
| ticker.advance(1, MILLISECONDS); |
| } |
| Set<Integer> keySet = cache.asMap().keySet(); |
| ASSERT.that(keySet).has().exactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); |
| |
| // 0 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(1, 2, 3, 4, 5, 6, 7, 8, 9); |
| |
| // reorder |
| getAll(cache, asList(0, 1, 2)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(2, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(3, 4, 5, 6, 7, 8, 9, 0, 1, 2); |
| |
| // 3 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(4, 5, 6, 7, 8, 9, 0, 1, 2); |
| |
| // reorder |
| getAll(cache, asList(5, 7, 9)); |
| CacheTesting.drainRecencyQueues(cache); |
| ASSERT.that(keySet).has().exactly(4, 6, 8, 0, 1, 2, 5, 7, 9); |
| |
| // 4 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(6, 8, 0, 1, 2, 5, 7, 9); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(6, 8, 0, 1, 2, 5, 7, 9); |
| |
| // 6 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(8, 0, 1, 2, 5, 7, 9); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(8, 0, 1, 2, 5, 7, 9); |
| |
| // 8 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(0, 1, 2, 5, 7, 9); |
| } |
| |
| public void testExpirationOrder_write() throws ExecutionException { |
| // test lru within a single segment |
| FakeTicker ticker = new FakeTicker(); |
| IdentityLoader<Integer> loader = identityLoader(); |
| LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder() |
| .concurrencyLevel(1) |
| .expireAfterWrite(11, MILLISECONDS) |
| .ticker(ticker) |
| .build(loader); |
| for (int i = 0; i < 10; i++) { |
| cache.getUnchecked(i); |
| ticker.advance(1, MILLISECONDS); |
| } |
| Set<Integer> keySet = cache.asMap().keySet(); |
| ASSERT.that(keySet).has().exactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); |
| |
| // 0 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(1, 2, 3, 4, 5, 6, 7, 8, 9); |
| |
| // get doesn't stop 1 from expiring |
| getAll(cache, asList(0, 1, 2)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(2, 3, 4, 5, 6, 7, 8, 9, 0); |
| |
| // get(K, Callable) doesn't stop 2 from expiring |
| cache.get(2, Callables.returning(-2)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(3, 4, 5, 6, 7, 8, 9, 0); |
| |
| // asMap.put saves 3 |
| cache.asMap().put(3, -3); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(4, 5, 6, 7, 8, 9, 0, 3); |
| |
| // asMap.replace saves 4 |
| cache.asMap().replace(4, -4); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(5, 6, 7, 8, 9, 0, 3, 4); |
| |
| // 5 expires |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(6, 7, 8, 9, 0, 3, 4); |
| } |
| |
| public void testExpirationOrder_writeAccess() throws ExecutionException { |
| // test lru within a single segment |
| FakeTicker ticker = new FakeTicker(); |
| IdentityLoader<Integer> loader = identityLoader(); |
| LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder() |
| .concurrencyLevel(1) |
| .expireAfterWrite(5, MILLISECONDS) |
| .expireAfterAccess(3, MILLISECONDS) |
| .ticker(ticker) |
| .build(loader); |
| for (int i = 0; i < 5; i++) { |
| cache.getUnchecked(i); |
| } |
| ticker.advance(1, MILLISECONDS); |
| for (int i = 5; i < 10; i++) { |
| cache.getUnchecked(i); |
| } |
| ticker.advance(1, MILLISECONDS); |
| |
| Set<Integer> keySet = cache.asMap().keySet(); |
| ASSERT.that(keySet).has().exactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); |
| |
| // get saves 1, 3; 0, 2, 4 expire |
| getAll(cache, asList(1, 3)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(5, 6, 7, 8, 9, 1, 3); |
| |
| // get saves 6, 8; 5, 7, 9 expire |
| getAll(cache, asList(6, 8)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(1, 3, 6, 8); |
| |
| // get fails to save 1, put saves 3 |
| cache.asMap().put(3, -3); |
| getAll(cache, asList(1)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(6, 8, 3); |
| |
| // get(K, Callable) fails to save 8, replace saves 6 |
| cache.asMap().replace(6, -6); |
| cache.get(8, Callables.returning(-8)); |
| CacheTesting.drainRecencyQueues(cache); |
| ticker.advance(1, MILLISECONDS); |
| ASSERT.that(keySet).has().exactly(3, 6); |
| } |
| |
| private void runRemovalScheduler(LoadingCache<String, Integer> cache, |
| CountingRemovalListener<String, Integer> removalListener, |
| WatchedCreatorLoader loader, |
| FakeTicker ticker, String keyPrefix, long ttl) { |
| |
| int shift1 = 10 + VALUE_PREFIX; |
| loader.setValuePrefix(shift1); |
| // fill with initial data |
| for (int i = 0; i < 10; i++) { |
| assertEquals(Integer.valueOf(i + shift1), cache.getUnchecked(keyPrefix + i)); |
| } |
| assertEquals(10, CacheTesting.expirationQueueSize(cache)); |
| assertEquals(0, removalListener.getCount()); |
| |
| // wait, so that entries have just 10 ms to live |
| ticker.advance(ttl * 2 / 3, MILLISECONDS); |
| |
| assertEquals(10, CacheTesting.expirationQueueSize(cache)); |
| assertEquals(0, removalListener.getCount()); |
| |
| int shift2 = shift1 + 10; |
| loader.setValuePrefix(shift2); |
| // fill with new data - has to live for 20 ms more |
| for (int i = 0; i < 10; i++) { |
| cache.invalidate(keyPrefix + i); |
| assertEquals("key: " + keyPrefix + i, |
| Integer.valueOf(i + shift2), cache.getUnchecked(keyPrefix + i)); |
| } |
| assertEquals(10, CacheTesting.expirationQueueSize(cache)); |
| assertEquals(10, removalListener.getCount()); // these are the invalidated ones |
| |
| // old timeouts must expire after this wait |
| ticker.advance(ttl * 2 / 3, MILLISECONDS); |
| |
| assertEquals(10, CacheTesting.expirationQueueSize(cache)); |
| assertEquals(10, removalListener.getCount()); |
| |
| // check that new values are still there - they still have 10 ms to live |
| for (int i = 0; i < 10; i++) { |
| loader.reset(); |
| assertEquals(Integer.valueOf(i + shift2), cache.getUnchecked(keyPrefix + i)); |
| assertFalse("Creator should NOT have been called @#" + i, loader.wasCalled()); |
| } |
| assertEquals(10, removalListener.getCount()); |
| } |
| |
| private void getAll(LoadingCache<Integer, Integer> cache, List<Integer> keys) { |
| for (int i : keys) { |
| cache.getUnchecked(i); |
| } |
| } |
| |
| private static class WatchedCreatorLoader extends CacheLoader<String, Integer> { |
| boolean wasCalled = false; // must be set in load() |
| String keyPrefix = KEY_PREFIX; |
| int valuePrefix = VALUE_PREFIX; |
| |
| public WatchedCreatorLoader() { |
| } |
| |
| public void reset() { |
| wasCalled = false; |
| } |
| |
| public boolean wasCalled() { |
| return wasCalled; |
| } |
| |
| public void setKeyPrefix(String keyPrefix) { |
| this.keyPrefix = keyPrefix; |
| } |
| |
| public void setValuePrefix(int valuePrefix) { |
| this.valuePrefix = valuePrefix; |
| } |
| |
| @Override public Integer load(String key) { |
| wasCalled = true; |
| return valuePrefix + Integer.parseInt(key.substring(keyPrefix.length())); |
| } |
| } |
| } |