View Javadoc
1   /*
2    * Copyright (C) 2009 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.google.common.cache;
18  
19  import static com.google.common.cache.TestingCacheLoaders.constantLoader;
20  import static com.google.common.cache.TestingCacheLoaders.identityLoader;
21  import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener;
22  import static com.google.common.cache.TestingRemovalListeners.nullRemovalListener;
23  import static com.google.common.cache.TestingRemovalListeners.queuingRemovalListener;
24  import static com.google.common.cache.TestingWeighers.constantWeigher;
25  import static java.util.concurrent.TimeUnit.NANOSECONDS;
26  import static java.util.concurrent.TimeUnit.SECONDS;
27  
28  import com.google.common.annotations.GwtCompatible;
29  import com.google.common.annotations.GwtIncompatible;
30  import com.google.common.base.Ticker;
31  import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener;
32  import com.google.common.cache.TestingRemovalListeners.QueuingRemovalListener;
33  import com.google.common.collect.Maps;
34  import com.google.common.collect.Sets;
35  import com.google.common.testing.NullPointerTester;
36  
37  import junit.framework.TestCase;
38  
39  import java.util.Map;
40  import java.util.Random;
41  import java.util.Set;
42  import java.util.concurrent.CountDownLatch;
43  import java.util.concurrent.ExecutorService;
44  import java.util.concurrent.Executors;
45  import java.util.concurrent.TimeUnit;
46  import java.util.concurrent.atomic.AtomicBoolean;
47  import java.util.concurrent.atomic.AtomicInteger;
48  
49  /**
50   * Unit tests for CacheBuilder.
51   */
52  @GwtCompatible(emulated = true)
53  public class CacheBuilderTest extends TestCase {
54  
55    public void testNewBuilder() {
56      CacheLoader<Object, Integer> loader = constantLoader(1);
57  
58      LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
59          .removalListener(countingRemovalListener())
60          .build(loader);
61  
62      assertEquals(Integer.valueOf(1), cache.getUnchecked("one"));
63      assertEquals(1, cache.size());
64    }
65  
66    public void testInitialCapacity_negative() {
67      CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
68      try {
69        builder.initialCapacity(-1);
70        fail();
71      } catch (IllegalArgumentException expected) {}
72    }
73  
74    public void testInitialCapacity_setTwice() {
75      CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>().initialCapacity(16);
76      try {
77        // even to the same value is not allowed
78        builder.initialCapacity(16);
79        fail();
80      } catch (IllegalStateException expected) {}
81    }
82  
83    @GwtIncompatible("CacheTesting")
84    public void testInitialCapacity_small() {
85      LoadingCache<?, ?> cache = CacheBuilder.newBuilder()
86          .initialCapacity(5)
87          .build(identityLoader());
88      LocalCache<?, ?> map = CacheTesting.toLocalCache(cache);
89  
90      assertEquals(4, map.segments.length);
91      assertEquals(2, map.segments[0].table.length());
92      assertEquals(2, map.segments[1].table.length());
93      assertEquals(2, map.segments[2].table.length());
94      assertEquals(2, map.segments[3].table.length());
95    }
96  
97    @GwtIncompatible("CacheTesting")
98    public void testInitialCapacity_smallest() {
99      LoadingCache<?, ?> cache = CacheBuilder.newBuilder()
100         .initialCapacity(0)
101         .build(identityLoader());
102     LocalCache<?, ?> map = CacheTesting.toLocalCache(cache);
103 
104     assertEquals(4, map.segments.length);
105     // 1 is as low as it goes, not 0. it feels dirty to know this/test this.
106     assertEquals(1, map.segments[0].table.length());
107     assertEquals(1, map.segments[1].table.length());
108     assertEquals(1, map.segments[2].table.length());
109     assertEquals(1, map.segments[3].table.length());
110   }
111 
112   public void testInitialCapacity_large() {
113     CacheBuilder.newBuilder().initialCapacity(Integer.MAX_VALUE);
114     // that the builder didn't blow up is enough;
115     // don't actually create this monster!
116   }
117 
118   public void testConcurrencyLevel_zero() {
119     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
120     try {
121       builder.concurrencyLevel(0);
122       fail();
123     } catch (IllegalArgumentException expected) {}
124   }
125 
126   public void testConcurrencyLevel_setTwice() {
127     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>().concurrencyLevel(16);
128     try {
129       // even to the same value is not allowed
130       builder.concurrencyLevel(16);
131       fail();
132     } catch (IllegalStateException expected) {}
133   }
134 
135   @GwtIncompatible("CacheTesting")
136   public void testConcurrencyLevel_small() {
137     LoadingCache<?, ?> cache = CacheBuilder.newBuilder()
138         .concurrencyLevel(1)
139         .build(identityLoader());
140     LocalCache<?, ?> map = CacheTesting.toLocalCache(cache);
141     assertEquals(1, map.segments.length);
142   }
143 
144   public void testConcurrencyLevel_large() {
145     CacheBuilder.newBuilder().concurrencyLevel(Integer.MAX_VALUE);
146     // don't actually build this beast
147   }
148 
149   public void testMaximumSize_negative() {
150     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
151     try {
152       builder.maximumSize(-1);
153       fail();
154     } catch (IllegalArgumentException expected) {}
155   }
156 
157   public void testMaximumSize_setTwice() {
158     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>().maximumSize(16);
159     try {
160       // even to the same value is not allowed
161       builder.maximumSize(16);
162       fail();
163     } catch (IllegalStateException expected) {}
164   }
165 
166   @GwtIncompatible("maximumWeight")
167   public void testMaximumSize_andWeight() {
168     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>().maximumSize(16);
169     try {
170       builder.maximumWeight(16);
171       fail();
172     } catch (IllegalStateException expected) {}
173   }
174 
175   @GwtIncompatible("maximumWeight")
176   public void testMaximumWeight_negative() {
177     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
178     try {
179       builder.maximumWeight(-1);
180       fail();
181     } catch (IllegalArgumentException expected) {}
182   }
183 
184   @GwtIncompatible("maximumWeight")
185   public void testMaximumWeight_setTwice() {
186     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>().maximumWeight(16);
187     try {
188       // even to the same value is not allowed
189       builder.maximumWeight(16);
190       fail();
191     } catch (IllegalStateException expected) {}
192     try {
193       builder.maximumSize(16);
194       fail();
195     } catch (IllegalStateException expected) {}
196   }
197 
198   @GwtIncompatible("maximumWeight")
199   public void testMaximumWeight_withoutWeigher() {
200     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>()
201         .maximumWeight(1);
202     try {
203       builder.build(identityLoader());
204       fail();
205     } catch (IllegalStateException expected) {}
206   }
207 
208   @GwtIncompatible("weigher")
209   public void testWeigher_withoutMaximumWeight() {
210     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>()
211         .weigher(constantWeigher(42));
212     try {
213       builder.build(identityLoader());
214       fail();
215     } catch (IllegalStateException expected) {}
216   }
217 
218   @GwtIncompatible("weigher")
219   public void testWeigher_withMaximumSize() {
220     try {
221       CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>()
222           .weigher(constantWeigher(42))
223           .maximumSize(1);
224       fail();
225     } catch (IllegalStateException expected) {}
226     try {
227       CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>()
228           .maximumSize(1)
229           .weigher(constantWeigher(42));
230       fail();
231     } catch (IllegalStateException expected) {}
232   }
233 
234   @GwtIncompatible("weakKeys")
235   public void testKeyStrengthSetTwice() {
236     CacheBuilder<Object, Object> builder1 = new CacheBuilder<Object, Object>().weakKeys();
237     try {
238       builder1.weakKeys();
239       fail();
240     } catch (IllegalStateException expected) {}
241   }
242 
243   @GwtIncompatible("weakValues")
244   public void testValueStrengthSetTwice() {
245     CacheBuilder<Object, Object> builder1 = new CacheBuilder<Object, Object>().weakValues();
246     try {
247       builder1.weakValues();
248       fail();
249     } catch (IllegalStateException expected) {}
250     try {
251       builder1.softValues();
252       fail();
253     } catch (IllegalStateException expected) {}
254 
255     CacheBuilder<Object, Object> builder2 = new CacheBuilder<Object, Object>().softValues();
256     try {
257       builder2.softValues();
258       fail();
259     } catch (IllegalStateException expected) {}
260     try {
261       builder2.weakValues();
262       fail();
263     } catch (IllegalStateException expected) {}
264   }
265 
266   public void testTimeToLive_negative() {
267     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
268     try {
269       builder.expireAfterWrite(-1, SECONDS);
270       fail();
271     } catch (IllegalArgumentException expected) {}
272   }
273 
274   public void testTimeToLive_small() {
275     CacheBuilder.newBuilder()
276         .expireAfterWrite(1, NANOSECONDS)
277         .build(identityLoader());
278     // well, it didn't blow up.
279   }
280 
281   public void testTimeToLive_setTwice() {
282     CacheBuilder<Object, Object> builder =
283         new CacheBuilder<Object, Object>().expireAfterWrite(3600, SECONDS);
284     try {
285       // even to the same value is not allowed
286       builder.expireAfterWrite(3600, SECONDS);
287       fail();
288     } catch (IllegalStateException expected) {}
289   }
290 
291   public void testTimeToIdle_negative() {
292     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
293     try {
294       builder.expireAfterAccess(-1, SECONDS);
295       fail();
296     } catch (IllegalArgumentException expected) {}
297   }
298 
299   public void testTimeToIdle_small() {
300     CacheBuilder.newBuilder()
301         .expireAfterAccess(1, NANOSECONDS)
302         .build(identityLoader());
303     // well, it didn't blow up.
304   }
305 
306   public void testTimeToIdle_setTwice() {
307     CacheBuilder<Object, Object> builder =
308         new CacheBuilder<Object, Object>().expireAfterAccess(3600, SECONDS);
309     try {
310       // even to the same value is not allowed
311       builder.expireAfterAccess(3600, SECONDS);
312       fail();
313     } catch (IllegalStateException expected) {}
314   }
315 
316   public void testTimeToIdleAndToLive() {
317     CacheBuilder.newBuilder()
318         .expireAfterWrite(1, NANOSECONDS)
319         .expireAfterAccess(1, NANOSECONDS)
320         .build(identityLoader());
321     // well, it didn't blow up.
322   }
323 
324   @GwtIncompatible("refreshAfterWrite")
325   public void testRefresh_zero() {
326     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
327     try {
328       builder.refreshAfterWrite(0, SECONDS);
329       fail();
330     } catch (IllegalArgumentException expected) {}
331   }
332 
333   @GwtIncompatible("refreshAfterWrite")
334   public void testRefresh_setTwice() {
335     CacheBuilder<Object, Object> builder =
336         new CacheBuilder<Object, Object>().refreshAfterWrite(3600, SECONDS);
337     try {
338       // even to the same value is not allowed
339       builder.refreshAfterWrite(3600, SECONDS);
340       fail();
341     } catch (IllegalStateException expected) {}
342   }
343 
344   public void testTicker_setTwice() {
345     Ticker testTicker = Ticker.systemTicker();
346     CacheBuilder<Object, Object> builder =
347         new CacheBuilder<Object, Object>().ticker(testTicker);
348     try {
349       // even to the same instance is not allowed
350       builder.ticker(testTicker);
351       fail();
352     } catch (IllegalStateException expected) {}
353   }
354 
355   public void testRemovalListener_setTwice() {
356     RemovalListener<Object, Object> testListener = nullRemovalListener();
357     CacheBuilder<Object, Object> builder =
358         new CacheBuilder<Object, Object>().removalListener(testListener);
359     try {
360       // even to the same instance is not allowed
361       builder = builder.removalListener(testListener);
362       fail();
363     } catch (IllegalStateException expected) {}
364   }
365 
366   @GwtIncompatible("CacheTesting")
367   public void testNullCache() {
368     CountingRemovalListener<Object, Object> listener = countingRemovalListener();
369     LoadingCache<Object, Object> nullCache = new CacheBuilder<Object, Object>()
370         .maximumSize(0)
371         .removalListener(listener)
372         .build(identityLoader());
373     assertEquals(0, nullCache.size());
374     Object key = new Object();
375     assertSame(key, nullCache.getUnchecked(key));
376     assertEquals(1, listener.getCount());
377     assertEquals(0, nullCache.size());
378     CacheTesting.checkEmpty(nullCache.asMap());
379   }
380 
381   @GwtIncompatible("QueuingRemovalListener")
382 
383   public void testRemovalNotification_clear() throws InterruptedException {
384     // If a clear() happens while a computation is pending, we should not get a removal
385     // notification.
386 
387     final AtomicBoolean shouldWait = new AtomicBoolean(false);
388     final CountDownLatch computingLatch = new CountDownLatch(1);
389     CacheLoader<String, String> computingFunction = new CacheLoader<String, String>() {
390       @Override public String load(String key) throws InterruptedException {
391         if (shouldWait.get()) {
392           computingLatch.await();
393         }
394         return key;
395       }
396     };
397     QueuingRemovalListener<String, String> listener = queuingRemovalListener();
398 
399     final LoadingCache<String, String> cache = CacheBuilder.newBuilder()
400         .concurrencyLevel(1)
401         .removalListener(listener)
402         .build(computingFunction);
403 
404     // seed the map, so its segment's count > 0
405     cache.getUnchecked("a");
406     shouldWait.set(true);
407 
408     final CountDownLatch computationStarted = new CountDownLatch(1);
409     final CountDownLatch computationComplete = new CountDownLatch(1);
410     new Thread(new Runnable() {
411       @Override public void run() {
412         computationStarted.countDown();
413         cache.getUnchecked("b");
414         computationComplete.countDown();
415       }
416     }).start();
417 
418     // wait for the computingEntry to be created
419     computationStarted.await();
420     cache.invalidateAll();
421     // let the computation proceed
422     computingLatch.countDown();
423     // don't check cache.size() until we know the get("b") call is complete
424     computationComplete.await();
425 
426     // At this point, the listener should be holding the seed value (a -> a), and the map should
427     // contain the computed value (b -> b), since the clear() happened before the computation
428     // completed.
429     assertEquals(1, listener.size());
430     RemovalNotification<String, String> notification = listener.remove();
431     assertEquals("a", notification.getKey());
432     assertEquals("a", notification.getValue());
433     assertEquals(1, cache.size());
434     assertEquals("b", cache.getUnchecked("b"));
435   }
436 
437   // "Basher tests", where we throw a bunch of stuff at a LoadingCache and check basic invariants.
438 
439   /**
440    * This is a less carefully-controlled version of {@link #testRemovalNotification_clear} - this is
441    * a black-box test that tries to create lots of different thread-interleavings, and asserts that
442    * each computation is affected by a call to {@code clear()} (and therefore gets passed to the
443    * removal listener), or else is not affected by the {@code clear()} (and therefore exists in the
444    * cache afterward).
445    */
446   @GwtIncompatible("QueuingRemovalListener")
447 
448   public void testRemovalNotification_clear_basher() throws InterruptedException {
449     // If a clear() happens close to the end of computation, one of two things should happen:
450     // - computation ends first: the removal listener is called, and the cache does not contain the
451     //   key/value pair
452     // - clear() happens first: the removal listener is not called, and the cache contains the pair
453     AtomicBoolean computationShouldWait = new AtomicBoolean();
454     CountDownLatch computationLatch = new CountDownLatch(1);
455     QueuingRemovalListener<String, String> listener = queuingRemovalListener();
456     final LoadingCache <String, String> cache = CacheBuilder.newBuilder()
457         .removalListener(listener)
458         .concurrencyLevel(20)
459         .build(
460             new DelayingIdentityLoader<String>(computationShouldWait, computationLatch));
461 
462     int nThreads = 100;
463     int nTasks = 1000;
464     int nSeededEntries = 100;
465     Set<String> expectedKeys = Sets.newHashSetWithExpectedSize(nTasks + nSeededEntries);
466     // seed the map, so its segments have a count>0; otherwise, clear() won't visit the in-progress
467     // entries
468     for (int i = 0; i < nSeededEntries; i++) {
469       String s = "b" + i;
470       cache.getUnchecked(s);
471       expectedKeys.add(s);
472     }
473     computationShouldWait.set(true);
474 
475     final AtomicInteger computedCount = new AtomicInteger();
476     ExecutorService threadPool = Executors.newFixedThreadPool(nThreads);
477     final CountDownLatch tasksFinished = new CountDownLatch(nTasks);
478     for (int i = 0; i < nTasks; i++) {
479       final String s = "a" + i;
480       threadPool.submit(new Runnable() {
481         @Override public void run() {
482           cache.getUnchecked(s);
483           computedCount.incrementAndGet();
484           tasksFinished.countDown();
485         }
486       });
487       expectedKeys.add(s);
488     }
489 
490     computationLatch.countDown();
491     // let some computations complete
492     while (computedCount.get() < nThreads) {
493       Thread.yield();
494     }
495     cache.invalidateAll();
496     tasksFinished.await();
497 
498     // Check all of the removal notifications we received: they should have had correctly-associated
499     // keys and values. (An earlier bug saw removal notifications for in-progress computations,
500     // which had real keys with null values.)
501     Map<String, String> removalNotifications = Maps.newHashMap();
502     for (RemovalNotification<String, String> notification : listener) {
503       removalNotifications.put(notification.getKey(), notification.getValue());
504       assertEquals("Unexpected key/value pair passed to removalListener",
505           notification.getKey(), notification.getValue());
506     }
507 
508     // All of the seed values should have been visible, so we should have gotten removal
509     // notifications for all of them.
510     for (int i = 0; i < nSeededEntries; i++) {
511       assertEquals("b" + i, removalNotifications.get("b" + i));
512     }
513 
514     // Each of the values added to the map should either still be there, or have seen a removal
515     // notification.
516     assertEquals(expectedKeys, Sets.union(cache.asMap().keySet(), removalNotifications.keySet()));
517     assertTrue(Sets.intersection(cache.asMap().keySet(), removalNotifications.keySet()).isEmpty());
518   }
519 
520   /**
521    * Calls get() repeatedly from many different threads, and tests that all of the removed entries
522    * (removed because of size limits or expiration) trigger appropriate removal notifications.
523    */
524   @GwtIncompatible("QueuingRemovalListener")
525 
526   public void testRemovalNotification_get_basher() throws InterruptedException {
527     int nTasks = 1000;
528     int nThreads = 100;
529     final int getsPerTask = 1000;
530     final int nUniqueKeys = 10000;
531     final Random random = new Random(); // Randoms.insecureRandom();
532 
533     QueuingRemovalListener<String, String> removalListener = queuingRemovalListener();
534     final AtomicInteger computeCount = new AtomicInteger();
535     final AtomicInteger exceptionCount = new AtomicInteger();
536     final AtomicInteger computeNullCount = new AtomicInteger();
537     CacheLoader<String, String> countingIdentityLoader =
538         new CacheLoader<String, String>() {
539           @Override public String load(String key) throws InterruptedException {
540             int behavior = random.nextInt(4);
541             if (behavior == 0) { // throw an exception
542               exceptionCount.incrementAndGet();
543               throw new RuntimeException("fake exception for test");
544             } else if (behavior == 1) { // return null
545               computeNullCount.incrementAndGet();
546               return null;
547             } else if (behavior == 2) { // slight delay before returning
548               Thread.sleep(5);
549               computeCount.incrementAndGet();
550               return key;
551             } else {
552               computeCount.incrementAndGet();
553               return key;
554             }
555           }
556         };
557     final LoadingCache<String, String> cache = CacheBuilder.newBuilder()
558         .recordStats()
559         .concurrencyLevel(2)
560         .expireAfterWrite(100, TimeUnit.MILLISECONDS)
561         .removalListener(removalListener)
562         .maximumSize(5000)
563         .build(countingIdentityLoader);
564 
565     ExecutorService threadPool = Executors.newFixedThreadPool(nThreads);
566     for (int i = 0; i < nTasks; i++) {
567       threadPool.submit(new Runnable() {
568         @Override public void run() {
569           for (int j = 0; j < getsPerTask; j++) {
570             try {
571               cache.getUnchecked("key" + random.nextInt(nUniqueKeys));
572             } catch (RuntimeException e) {
573             }
574           }
575         }
576       });
577     }
578 
579     threadPool.shutdown();
580     threadPool.awaitTermination(300, TimeUnit.SECONDS);
581 
582     // Since we're not doing any more cache operations, and the cache only expires/evicts when doing
583     // other operations, the cache and the removal queue won't change from this point on.
584 
585     // Verify that each received removal notification was valid
586     for (RemovalNotification<String, String> notification : removalListener) {
587       assertEquals("Invalid removal notification", notification.getKey(), notification.getValue());
588     }
589 
590     CacheStats stats = cache.stats();
591     assertEquals(removalListener.size(), stats.evictionCount());
592     assertEquals(computeCount.get(), stats.loadSuccessCount());
593     assertEquals(exceptionCount.get() + computeNullCount.get(), stats.loadExceptionCount());
594     // each computed value is still in the cache, or was passed to the removal listener
595     assertEquals(computeCount.get(), cache.size() + removalListener.size());
596   }
597 
598   @GwtIncompatible("NullPointerTester")
599   public void testNullParameters() throws Exception {
600     NullPointerTester tester = new NullPointerTester();
601     CacheBuilder<Object, Object> builder = new CacheBuilder<Object, Object>();
602     tester.testAllPublicInstanceMethods(builder);
603   }
604 
605   @GwtIncompatible("CacheTesting")
606   public void testSizingDefaults() {
607     LoadingCache<?, ?> cache = CacheBuilder.newBuilder().build(identityLoader());
608     LocalCache<?, ?> map = CacheTesting.toLocalCache(cache);
609     assertEquals(4, map.segments.length); // concurrency level
610     assertEquals(4, map.segments[0].table.length()); // capacity / conc level
611   }
612 
613   @GwtIncompatible("CountDownLatch")
614   static final class DelayingIdentityLoader<T> extends CacheLoader<T, T> {
615     private final AtomicBoolean shouldWait;
616     private final CountDownLatch delayLatch;
617 
618     DelayingIdentityLoader(AtomicBoolean shouldWait, CountDownLatch delayLatch) {
619       this.shouldWait = shouldWait;
620       this.delayLatch = delayLatch;
621     }
622 
623     @Override public T load(T key) throws InterruptedException {
624       if (shouldWait.get()) {
625         delayLatch.await();
626       }
627       return key;
628     }
629   }
630 }