Home | History | Annotate | Download | only in cache
      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 }
    631