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