• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package com.google.common.cache;
16 
17 import static com.google.common.cache.TestingCacheLoaders.identityLoader;
18 import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener;
19 import static com.google.common.truth.Truth.assertThat;
20 import static java.util.Arrays.asList;
21 import static java.util.concurrent.TimeUnit.MILLISECONDS;
22 
23 import com.google.common.cache.TestingCacheLoaders.IdentityLoader;
24 import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener;
25 import com.google.common.cache.TestingRemovalListeners.QueuingRemovalListener;
26 import com.google.common.collect.Iterators;
27 import com.google.common.testing.FakeTicker;
28 import com.google.common.util.concurrent.Callables;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.atomic.AtomicInteger;
34 import junit.framework.TestCase;
35 
36 /**
37  * Tests relating to cache expiration: make sure entries expire at the right times, make sure
38  * expired entries don't show up, etc.
39  *
40  * @author mike nonemacher
41  */
42 @SuppressWarnings("deprecation") // tests of deprecated method
43 public class CacheExpirationTest extends TestCase {
44 
45   private static final long EXPIRING_TIME = 1000;
46   private static final int VALUE_PREFIX = 12345;
47   private static final String KEY_PREFIX = "key prefix:";
48 
testExpiration_expireAfterWrite()49   public void testExpiration_expireAfterWrite() {
50     FakeTicker ticker = new FakeTicker();
51     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
52     WatchedCreatorLoader loader = new WatchedCreatorLoader();
53     LoadingCache<String, Integer> cache =
54         CacheBuilder.newBuilder()
55             .expireAfterWrite(EXPIRING_TIME, MILLISECONDS)
56             .removalListener(removalListener)
57             .ticker(ticker)
58             .build(loader);
59     checkExpiration(cache, loader, ticker, removalListener);
60   }
61 
testExpiration_expireAfterAccess()62   public void testExpiration_expireAfterAccess() {
63     FakeTicker ticker = new FakeTicker();
64     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
65     WatchedCreatorLoader loader = new WatchedCreatorLoader();
66     LoadingCache<String, Integer> cache =
67         CacheBuilder.newBuilder()
68             .expireAfterAccess(EXPIRING_TIME, MILLISECONDS)
69             .removalListener(removalListener)
70             .ticker(ticker)
71             .build(loader);
72     checkExpiration(cache, loader, ticker, removalListener);
73   }
74 
checkExpiration( LoadingCache<String, Integer> cache, WatchedCreatorLoader loader, FakeTicker ticker, CountingRemovalListener<String, Integer> removalListener)75   private void checkExpiration(
76       LoadingCache<String, Integer> cache,
77       WatchedCreatorLoader loader,
78       FakeTicker ticker,
79       CountingRemovalListener<String, Integer> removalListener) {
80 
81     for (int i = 0; i < 10; i++) {
82       assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i));
83     }
84 
85     for (int i = 0; i < 10; i++) {
86       loader.reset();
87       assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i));
88       assertFalse("Creator should not have been called @#" + i, loader.wasCalled());
89     }
90 
91     CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker);
92 
93     assertEquals("Map must be empty by now", 0, cache.size());
94     assertEquals("Eviction notifications must be received", 10, removalListener.getCount());
95 
96     CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker);
97     // ensure that no new notifications are sent
98     assertEquals("Eviction notifications must be received", 10, removalListener.getCount());
99   }
100 
testExpiringGet_expireAfterWrite()101   public void testExpiringGet_expireAfterWrite() {
102     FakeTicker ticker = new FakeTicker();
103     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
104     WatchedCreatorLoader loader = new WatchedCreatorLoader();
105     LoadingCache<String, Integer> cache =
106         CacheBuilder.newBuilder()
107             .expireAfterWrite(EXPIRING_TIME, MILLISECONDS)
108             .removalListener(removalListener)
109             .ticker(ticker)
110             .build(loader);
111     runExpirationTest(cache, loader, ticker, removalListener);
112   }
113 
testExpiringGet_expireAfterAccess()114   public void testExpiringGet_expireAfterAccess() {
115     FakeTicker ticker = new FakeTicker();
116     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
117     WatchedCreatorLoader loader = new WatchedCreatorLoader();
118     LoadingCache<String, Integer> cache =
119         CacheBuilder.newBuilder()
120             .expireAfterAccess(EXPIRING_TIME, MILLISECONDS)
121             .removalListener(removalListener)
122             .ticker(ticker)
123             .build(loader);
124     runExpirationTest(cache, loader, ticker, removalListener);
125   }
126 
runExpirationTest( LoadingCache<String, Integer> cache, WatchedCreatorLoader loader, FakeTicker ticker, CountingRemovalListener<String, Integer> removalListener)127   private void runExpirationTest(
128       LoadingCache<String, Integer> cache,
129       WatchedCreatorLoader loader,
130       FakeTicker ticker,
131       CountingRemovalListener<String, Integer> removalListener) {
132 
133     for (int i = 0; i < 10; i++) {
134       assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i));
135     }
136 
137     for (int i = 0; i < 10; i++) {
138       loader.reset();
139       assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i));
140       assertFalse("Loader should NOT have been called @#" + i, loader.wasCalled());
141     }
142 
143     // wait for entries to expire, but don't call expireEntries
144     ticker.advance(EXPIRING_TIME * 10, MILLISECONDS);
145 
146     // add a single unexpired entry
147     cache.getUnchecked(KEY_PREFIX + 11);
148 
149     // collections views shouldn't expose expired entries
150     assertEquals(1, Iterators.size(cache.asMap().entrySet().iterator()));
151     assertEquals(1, Iterators.size(cache.asMap().keySet().iterator()));
152     assertEquals(1, Iterators.size(cache.asMap().values().iterator()));
153 
154     CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker);
155 
156     for (int i = 0; i < 11; i++) {
157       assertFalse(cache.asMap().containsKey(KEY_PREFIX + i));
158     }
159     assertEquals(11, removalListener.getCount());
160 
161     for (int i = 0; i < 10; i++) {
162       assertFalse(cache.asMap().containsKey(KEY_PREFIX + i));
163       loader.reset();
164       assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i));
165       assertTrue("Creator should have been called @#" + i, loader.wasCalled());
166     }
167 
168     // expire new values we just created
169     CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker);
170     assertEquals("Eviction notifications must be received", 21, removalListener.getCount());
171 
172     CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker);
173     // ensure that no new notifications are sent
174     assertEquals("Eviction notifications must be received", 21, removalListener.getCount());
175   }
176 
testRemovalListener_expireAfterWrite()177   public void testRemovalListener_expireAfterWrite() {
178     FakeTicker ticker = new FakeTicker();
179     final AtomicInteger evictionCount = new AtomicInteger();
180     final AtomicInteger applyCount = new AtomicInteger();
181     final AtomicInteger totalSum = new AtomicInteger();
182 
183     RemovalListener<Integer, AtomicInteger> removalListener =
184         new RemovalListener<Integer, AtomicInteger>() {
185           @Override
186           public void onRemoval(RemovalNotification<Integer, AtomicInteger> notification) {
187             if (notification.wasEvicted()) {
188               evictionCount.incrementAndGet();
189               totalSum.addAndGet(notification.getValue().get());
190             }
191           }
192         };
193 
194     CacheLoader<Integer, AtomicInteger> loader =
195         new CacheLoader<Integer, AtomicInteger>() {
196           @Override
197           public AtomicInteger load(Integer key) {
198             applyCount.incrementAndGet();
199             return new AtomicInteger();
200           }
201         };
202 
203     LoadingCache<Integer, AtomicInteger> cache =
204         CacheBuilder.newBuilder()
205             .removalListener(removalListener)
206             .expireAfterWrite(10, MILLISECONDS)
207             .ticker(ticker)
208             .build(loader);
209 
210     // Increment 100 times
211     for (int i = 0; i < 100; ++i) {
212       cache.getUnchecked(10).incrementAndGet();
213       ticker.advance(1, MILLISECONDS);
214     }
215 
216     assertEquals(evictionCount.get() + 1, applyCount.get());
217     int remaining = cache.getUnchecked(10).get();
218     assertEquals(100, totalSum.get() + remaining);
219   }
220 
testRemovalScheduler_expireAfterWrite()221   public void testRemovalScheduler_expireAfterWrite() {
222     FakeTicker ticker = new FakeTicker();
223     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
224     WatchedCreatorLoader loader = new WatchedCreatorLoader();
225     LoadingCache<String, Integer> cache =
226         CacheBuilder.newBuilder()
227             .expireAfterWrite(EXPIRING_TIME, MILLISECONDS)
228             .removalListener(removalListener)
229             .ticker(ticker)
230             .build(loader);
231     runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME);
232   }
233 
testRemovalScheduler_expireAfterAccess()234   public void testRemovalScheduler_expireAfterAccess() {
235     FakeTicker ticker = new FakeTicker();
236     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
237     WatchedCreatorLoader loader = new WatchedCreatorLoader();
238     LoadingCache<String, Integer> cache =
239         CacheBuilder.newBuilder()
240             .expireAfterAccess(EXPIRING_TIME, MILLISECONDS)
241             .removalListener(removalListener)
242             .ticker(ticker)
243             .build(loader);
244     runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME);
245   }
246 
testRemovalScheduler_expireAfterBoth()247   public void testRemovalScheduler_expireAfterBoth() {
248     FakeTicker ticker = new FakeTicker();
249     CountingRemovalListener<String, Integer> removalListener = countingRemovalListener();
250     WatchedCreatorLoader loader = new WatchedCreatorLoader();
251     LoadingCache<String, Integer> cache =
252         CacheBuilder.newBuilder()
253             .expireAfterAccess(EXPIRING_TIME, MILLISECONDS)
254             .expireAfterWrite(EXPIRING_TIME, MILLISECONDS)
255             .removalListener(removalListener)
256             .ticker(ticker)
257             .build(loader);
258     runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME);
259   }
260 
testExpirationOrder_access()261   public void testExpirationOrder_access() {
262     // test lru within a single segment
263     FakeTicker ticker = new FakeTicker();
264     IdentityLoader<Integer> loader = identityLoader();
265     LoadingCache<Integer, Integer> cache =
266         CacheBuilder.newBuilder()
267             .concurrencyLevel(1)
268             .expireAfterAccess(11, MILLISECONDS)
269             .ticker(ticker)
270             .build(loader);
271     for (int i = 0; i < 10; i++) {
272       cache.getUnchecked(i);
273       ticker.advance(1, MILLISECONDS);
274     }
275     Set<Integer> keySet = cache.asMap().keySet();
276     assertThat(keySet).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
277 
278     // 0 expires
279     ticker.advance(1, MILLISECONDS);
280     assertThat(keySet).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9);
281 
282     // reorder
283     getAll(cache, asList(0, 1, 2));
284     CacheTesting.drainRecencyQueues(cache);
285     ticker.advance(2, MILLISECONDS);
286     assertThat(keySet).containsExactly(3, 4, 5, 6, 7, 8, 9, 0, 1, 2);
287 
288     // 3 expires
289     ticker.advance(1, MILLISECONDS);
290     assertThat(keySet).containsExactly(4, 5, 6, 7, 8, 9, 0, 1, 2);
291 
292     // reorder
293     getAll(cache, asList(5, 7, 9));
294     CacheTesting.drainRecencyQueues(cache);
295     assertThat(keySet).containsExactly(4, 6, 8, 0, 1, 2, 5, 7, 9);
296 
297     // 4 expires
298     ticker.advance(1, MILLISECONDS);
299     assertThat(keySet).containsExactly(6, 8, 0, 1, 2, 5, 7, 9);
300     ticker.advance(1, MILLISECONDS);
301     assertThat(keySet).containsExactly(6, 8, 0, 1, 2, 5, 7, 9);
302 
303     // 6 expires
304     ticker.advance(1, MILLISECONDS);
305     assertThat(keySet).containsExactly(8, 0, 1, 2, 5, 7, 9);
306     ticker.advance(1, MILLISECONDS);
307     assertThat(keySet).containsExactly(8, 0, 1, 2, 5, 7, 9);
308 
309     // 8 expires
310     ticker.advance(1, MILLISECONDS);
311     assertThat(keySet).containsExactly(0, 1, 2, 5, 7, 9);
312   }
313 
testExpirationOrder_write()314   public void testExpirationOrder_write() throws ExecutionException {
315     // test lru within a single segment
316     FakeTicker ticker = new FakeTicker();
317     IdentityLoader<Integer> loader = identityLoader();
318     LoadingCache<Integer, Integer> cache =
319         CacheBuilder.newBuilder()
320             .concurrencyLevel(1)
321             .expireAfterWrite(11, MILLISECONDS)
322             .ticker(ticker)
323             .build(loader);
324     for (int i = 0; i < 10; i++) {
325       cache.getUnchecked(i);
326       ticker.advance(1, MILLISECONDS);
327     }
328     Set<Integer> keySet = cache.asMap().keySet();
329     assertThat(keySet).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
330 
331     // 0 expires
332     ticker.advance(1, MILLISECONDS);
333     assertThat(keySet).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9);
334 
335     // get doesn't stop 1 from expiring
336     getAll(cache, asList(0, 1, 2));
337     CacheTesting.drainRecencyQueues(cache);
338     ticker.advance(1, MILLISECONDS);
339     assertThat(keySet).containsExactly(2, 3, 4, 5, 6, 7, 8, 9, 0);
340 
341     // get(K, Callable) doesn't stop 2 from expiring
342     cache.get(2, Callables.returning(-2));
343     CacheTesting.drainRecencyQueues(cache);
344     ticker.advance(1, MILLISECONDS);
345     assertThat(keySet).containsExactly(3, 4, 5, 6, 7, 8, 9, 0);
346 
347     // asMap.put saves 3
348     cache.asMap().put(3, -3);
349     ticker.advance(1, MILLISECONDS);
350     assertThat(keySet).containsExactly(4, 5, 6, 7, 8, 9, 0, 3);
351 
352     // asMap.replace saves 4
353     cache.asMap().replace(4, -4);
354     ticker.advance(1, MILLISECONDS);
355     assertThat(keySet).containsExactly(5, 6, 7, 8, 9, 0, 3, 4);
356 
357     // 5 expires
358     ticker.advance(1, MILLISECONDS);
359     assertThat(keySet).containsExactly(6, 7, 8, 9, 0, 3, 4);
360   }
361 
testExpirationOrder_writeAccess()362   public void testExpirationOrder_writeAccess() throws ExecutionException {
363     // test lru within a single segment
364     FakeTicker ticker = new FakeTicker();
365     IdentityLoader<Integer> loader = identityLoader();
366     LoadingCache<Integer, Integer> cache =
367         CacheBuilder.newBuilder()
368             .concurrencyLevel(1)
369             .expireAfterWrite(5, MILLISECONDS)
370             .expireAfterAccess(3, MILLISECONDS)
371             .ticker(ticker)
372             .build(loader);
373     for (int i = 0; i < 5; i++) {
374       cache.getUnchecked(i);
375     }
376     ticker.advance(1, MILLISECONDS);
377     for (int i = 5; i < 10; i++) {
378       cache.getUnchecked(i);
379     }
380     ticker.advance(1, MILLISECONDS);
381 
382     Set<Integer> keySet = cache.asMap().keySet();
383     assertThat(keySet).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
384 
385     // get saves 1, 3; 0, 2, 4 expire
386     getAll(cache, asList(1, 3));
387     CacheTesting.drainRecencyQueues(cache);
388     ticker.advance(1, MILLISECONDS);
389     assertThat(keySet).containsExactly(5, 6, 7, 8, 9, 1, 3);
390 
391     // get saves 6, 8; 5, 7, 9 expire
392     getAll(cache, asList(6, 8));
393     CacheTesting.drainRecencyQueues(cache);
394     ticker.advance(1, MILLISECONDS);
395     assertThat(keySet).containsExactly(1, 3, 6, 8);
396 
397     // get fails to save 1, put saves 3
398     cache.asMap().put(3, -3);
399     getAll(cache, asList(1));
400     CacheTesting.drainRecencyQueues(cache);
401     ticker.advance(1, MILLISECONDS);
402     assertThat(keySet).containsExactly(6, 8, 3);
403 
404     // get(K, Callable) fails to save 8, replace saves 6
405     cache.asMap().replace(6, -6);
406     cache.get(8, Callables.returning(-8));
407     CacheTesting.drainRecencyQueues(cache);
408     ticker.advance(1, MILLISECONDS);
409     assertThat(keySet).containsExactly(3, 6);
410   }
411 
testExpiration_invalidateAll()412   public void testExpiration_invalidateAll() {
413     FakeTicker ticker = new FakeTicker();
414     QueuingRemovalListener<Integer, Integer> listener =
415         TestingRemovalListeners.queuingRemovalListener();
416     Cache<Integer, Integer> cache =
417         CacheBuilder.newBuilder()
418             .expireAfterAccess(1, TimeUnit.MINUTES)
419             .removalListener(listener)
420             .ticker(ticker)
421             .build();
422     cache.put(1, 1);
423     ticker.advance(10, TimeUnit.MINUTES);
424     cache.invalidateAll();
425 
426     assertThat(listener.poll().getCause()).isEqualTo(RemovalCause.EXPIRED);
427   }
428 
runRemovalScheduler( LoadingCache<String, Integer> cache, CountingRemovalListener<String, Integer> removalListener, WatchedCreatorLoader loader, FakeTicker ticker, String keyPrefix, long ttl)429   private void runRemovalScheduler(
430       LoadingCache<String, Integer> cache,
431       CountingRemovalListener<String, Integer> removalListener,
432       WatchedCreatorLoader loader,
433       FakeTicker ticker,
434       String keyPrefix,
435       long ttl) {
436 
437     int shift1 = 10 + VALUE_PREFIX;
438     loader.setValuePrefix(shift1);
439     // fill with initial data
440     for (int i = 0; i < 10; i++) {
441       assertEquals(Integer.valueOf(i + shift1), cache.getUnchecked(keyPrefix + i));
442     }
443     assertEquals(10, CacheTesting.expirationQueueSize(cache));
444     assertEquals(0, removalListener.getCount());
445 
446     // wait, so that entries have just 10 ms to live
447     ticker.advance(ttl * 2 / 3, MILLISECONDS);
448 
449     assertEquals(10, CacheTesting.expirationQueueSize(cache));
450     assertEquals(0, removalListener.getCount());
451 
452     int shift2 = shift1 + 10;
453     loader.setValuePrefix(shift2);
454     // fill with new data - has to live for 20 ms more
455     for (int i = 0; i < 10; i++) {
456       cache.invalidate(keyPrefix + i);
457       assertEquals(
458           "key: " + keyPrefix + i, Integer.valueOf(i + shift2), cache.getUnchecked(keyPrefix + i));
459     }
460     assertEquals(10, CacheTesting.expirationQueueSize(cache));
461     assertEquals(10, removalListener.getCount()); // these are the invalidated ones
462 
463     // old timeouts must expire after this wait
464     ticker.advance(ttl * 2 / 3, MILLISECONDS);
465 
466     assertEquals(10, CacheTesting.expirationQueueSize(cache));
467     assertEquals(10, removalListener.getCount());
468 
469     // check that new values are still there - they still have 10 ms to live
470     for (int i = 0; i < 10; i++) {
471       loader.reset();
472       assertEquals(Integer.valueOf(i + shift2), cache.getUnchecked(keyPrefix + i));
473       assertFalse("Creator should NOT have been called @#" + i, loader.wasCalled());
474     }
475     assertEquals(10, removalListener.getCount());
476   }
477 
getAll(LoadingCache<Integer, Integer> cache, List<Integer> keys)478   private static void getAll(LoadingCache<Integer, Integer> cache, List<Integer> keys) {
479     for (int i : keys) {
480       cache.getUnchecked(i);
481     }
482   }
483 
484   private static class WatchedCreatorLoader extends CacheLoader<String, Integer> {
485     boolean wasCalled = false; // must be set in load()
486     String keyPrefix = KEY_PREFIX;
487     int valuePrefix = VALUE_PREFIX;
488 
WatchedCreatorLoader()489     public WatchedCreatorLoader() {}
490 
reset()491     public void reset() {
492       wasCalled = false;
493     }
494 
wasCalled()495     public boolean wasCalled() {
496       return wasCalled;
497     }
498 
setKeyPrefix(String keyPrefix)499     public void setKeyPrefix(String keyPrefix) {
500       this.keyPrefix = keyPrefix;
501     }
502 
setValuePrefix(int valuePrefix)503     public void setValuePrefix(int valuePrefix) {
504       this.valuePrefix = valuePrefix;
505     }
506 
507     @Override
load(String key)508     public Integer load(String key) {
509       wasCalled = true;
510       return valuePrefix + Integer.parseInt(key.substring(keyPrefix.length()));
511     }
512   }
513 }
514