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
17package com.google.common.cache;
18
19import static com.google.common.cache.TestingCacheLoaders.constantLoader;
20import static com.google.common.cache.TestingCacheLoaders.identityLoader;
21import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener;
22import static com.google.common.cache.TestingRemovalListeners.nullRemovalListener;
23import static com.google.common.cache.TestingRemovalListeners.queuingRemovalListener;
24import static com.google.common.cache.TestingWeighers.constantWeigher;
25import static java.util.concurrent.TimeUnit.NANOSECONDS;
26import static java.util.concurrent.TimeUnit.SECONDS;
27
28import com.google.common.annotations.GwtCompatible;
29import com.google.common.annotations.GwtIncompatible;
30import com.google.common.base.Ticker;
31import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener;
32import com.google.common.cache.TestingRemovalListeners.QueuingRemovalListener;
33import com.google.common.collect.Maps;
34import com.google.common.collect.Sets;
35import com.google.common.testing.NullPointerTester;
36
37import junit.framework.TestCase;
38
39import java.util.Map;
40import java.util.Random;
41import java.util.Set;
42import java.util.concurrent.CountDownLatch;
43import java.util.concurrent.ExecutorService;
44import java.util.concurrent.Executors;
45import java.util.concurrent.TimeUnit;
46import java.util.concurrent.atomic.AtomicBoolean;
47import java.util.concurrent.atomic.AtomicInteger;
48
49/**
50 * Unit tests for CacheBuilder.
51 */
52@GwtCompatible(emulated = true)
53public 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}
631