/* * 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.junit.contrib.truth.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).hasContentsAnyOrder(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); // 0 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(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).hasContentsAnyOrder(3, 4, 5, 6, 7, 8, 9, 0, 1, 2); // 3 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(4, 5, 6, 7, 8, 9, 0, 1, 2); // reorder getAll(cache, asList(5, 7, 9)); CacheTesting.drainRecencyQueues(cache); ASSERT.that(keySet).hasContentsAnyOrder(4, 6, 8, 0, 1, 2, 5, 7, 9); // 4 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(6, 8, 0, 1, 2, 5, 7, 9); ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(6, 8, 0, 1, 2, 5, 7, 9); // 6 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(8, 0, 1, 2, 5, 7, 9); ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(8, 0, 1, 2, 5, 7, 9); // 8 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(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).hasContentsAnyOrder(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); // 0 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(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).hasContentsAnyOrder(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).hasContentsAnyOrder(3, 4, 5, 6, 7, 8, 9, 0); // asMap.put saves 3 cache.asMap().put(3, -3); ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(4, 5, 6, 7, 8, 9, 0, 3); // asMap.replace saves 4 cache.asMap().replace(4, -4); ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(5, 6, 7, 8, 9, 0, 3, 4); // 5 expires ticker.advance(1, MILLISECONDS); ASSERT.that(keySet).hasContentsAnyOrder(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).hasContentsAnyOrder(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).hasContentsAnyOrder(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).hasContentsAnyOrder(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).hasContentsAnyOrder(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).hasContentsAnyOrder(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())); } } }