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 testNewBuilder()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 testInitialCapacity_negative()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 testInitialCapacity_setTwice()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") testInitialCapacity_small()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") testInitialCapacity_smallest()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 testInitialCapacity_large()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 testConcurrencyLevel_zero()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 testConcurrencyLevel_setTwice()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") testConcurrencyLevel_small()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 testConcurrencyLevel_large()144 public void testConcurrencyLevel_large() { 145 CacheBuilder.newBuilder().concurrencyLevel(Integer.MAX_VALUE); 146 // don't actually build this beast 147 } 148 testMaximumSize_negative()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 testMaximumSize_setTwice()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") testMaximumSize_andWeight()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") testMaximumWeight_negative()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") testMaximumWeight_setTwice()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") testMaximumWeight_withoutWeigher()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") testWeigher_withoutMaximumWeight()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") testWeigher_withMaximumSize()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") testKeyStrengthSetTwice()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") testValueStrengthSetTwice()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 testTimeToLive_negative()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 testTimeToLive_small()274 public void testTimeToLive_small() { 275 CacheBuilder.newBuilder() 276 .expireAfterWrite(1, NANOSECONDS) 277 .build(identityLoader()); 278 // well, it didn't blow up. 279 } 280 testTimeToLive_setTwice()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 testTimeToIdle_negative()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 testTimeToIdle_small()299 public void testTimeToIdle_small() { 300 CacheBuilder.newBuilder() 301 .expireAfterAccess(1, NANOSECONDS) 302 .build(identityLoader()); 303 // well, it didn't blow up. 304 } 305 testTimeToIdle_setTwice()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 testTimeToIdleAndToLive()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") testRefresh_zero()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") testRefresh_setTwice()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 testTicker_setTwice()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 testRemovalListener_setTwice()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") testNullCache()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 testRemovalNotification_clear()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 testRemovalNotification_clear_basher()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 testRemovalNotification_get_basher()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") testNullParameters()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") testSizingDefaults()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 DelayingIdentityLoader(AtomicBoolean shouldWait, CountDownLatch delayLatch)618 DelayingIdentityLoader(AtomicBoolean shouldWait, CountDownLatch delayLatch) { 619 this.shouldWait = shouldWait; 620 this.delayLatch = delayLatch; 621 } 622 load(T key)623 @Override public T load(T key) throws InterruptedException { 624 if (shouldWait.get()) { 625 delayLatch.await(); 626 } 627 return key; 628 } 629 } 630 } 631