1 /* 2 * Copyright (C) 2011 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.google.common.cache; 16 17 import static com.google.common.cache.TestingCacheLoaders.identityLoader; 18 import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener; 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.util.Arrays.asList; 21 import static java.util.concurrent.TimeUnit.MILLISECONDS; 22 23 import com.google.common.cache.TestingCacheLoaders.IdentityLoader; 24 import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener; 25 import com.google.common.cache.TestingRemovalListeners.QueuingRemovalListener; 26 import com.google.common.collect.Iterators; 27 import com.google.common.testing.FakeTicker; 28 import com.google.common.util.concurrent.Callables; 29 import java.util.List; 30 import java.util.Set; 31 import java.util.concurrent.ExecutionException; 32 import java.util.concurrent.TimeUnit; 33 import java.util.concurrent.atomic.AtomicInteger; 34 import junit.framework.TestCase; 35 36 /** 37 * Tests relating to cache expiration: make sure entries expire at the right times, make sure 38 * expired entries don't show up, etc. 39 * 40 * @author mike nonemacher 41 */ 42 @SuppressWarnings("deprecation") // tests of deprecated method 43 public class CacheExpirationTest extends TestCase { 44 45 private static final long EXPIRING_TIME = 1000; 46 private static final int VALUE_PREFIX = 12345; 47 private static final String KEY_PREFIX = "key prefix:"; 48 testExpiration_expireAfterWrite()49 public void testExpiration_expireAfterWrite() { 50 FakeTicker ticker = new FakeTicker(); 51 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 52 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 53 LoadingCache<String, Integer> cache = 54 CacheBuilder.newBuilder() 55 .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) 56 .removalListener(removalListener) 57 .ticker(ticker) 58 .build(loader); 59 checkExpiration(cache, loader, ticker, removalListener); 60 } 61 testExpiration_expireAfterAccess()62 public void testExpiration_expireAfterAccess() { 63 FakeTicker ticker = new FakeTicker(); 64 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 65 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 66 LoadingCache<String, Integer> cache = 67 CacheBuilder.newBuilder() 68 .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) 69 .removalListener(removalListener) 70 .ticker(ticker) 71 .build(loader); 72 checkExpiration(cache, loader, ticker, removalListener); 73 } 74 checkExpiration( LoadingCache<String, Integer> cache, WatchedCreatorLoader loader, FakeTicker ticker, CountingRemovalListener<String, Integer> removalListener)75 private void checkExpiration( 76 LoadingCache<String, Integer> cache, 77 WatchedCreatorLoader loader, 78 FakeTicker ticker, 79 CountingRemovalListener<String, Integer> removalListener) { 80 81 for (int i = 0; i < 10; i++) { 82 assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); 83 } 84 85 for (int i = 0; i < 10; i++) { 86 loader.reset(); 87 assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); 88 assertFalse("Creator should not have been called @#" + i, loader.wasCalled()); 89 } 90 91 CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); 92 93 assertEquals("Map must be empty by now", 0, cache.size()); 94 assertEquals("Eviction notifications must be received", 10, removalListener.getCount()); 95 96 CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); 97 // ensure that no new notifications are sent 98 assertEquals("Eviction notifications must be received", 10, removalListener.getCount()); 99 } 100 testExpiringGet_expireAfterWrite()101 public void testExpiringGet_expireAfterWrite() { 102 FakeTicker ticker = new FakeTicker(); 103 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 104 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 105 LoadingCache<String, Integer> cache = 106 CacheBuilder.newBuilder() 107 .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) 108 .removalListener(removalListener) 109 .ticker(ticker) 110 .build(loader); 111 runExpirationTest(cache, loader, ticker, removalListener); 112 } 113 testExpiringGet_expireAfterAccess()114 public void testExpiringGet_expireAfterAccess() { 115 FakeTicker ticker = new FakeTicker(); 116 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 117 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 118 LoadingCache<String, Integer> cache = 119 CacheBuilder.newBuilder() 120 .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) 121 .removalListener(removalListener) 122 .ticker(ticker) 123 .build(loader); 124 runExpirationTest(cache, loader, ticker, removalListener); 125 } 126 runExpirationTest( LoadingCache<String, Integer> cache, WatchedCreatorLoader loader, FakeTicker ticker, CountingRemovalListener<String, Integer> removalListener)127 private void runExpirationTest( 128 LoadingCache<String, Integer> cache, 129 WatchedCreatorLoader loader, 130 FakeTicker ticker, 131 CountingRemovalListener<String, Integer> removalListener) { 132 133 for (int i = 0; i < 10; i++) { 134 assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); 135 } 136 137 for (int i = 0; i < 10; i++) { 138 loader.reset(); 139 assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); 140 assertFalse("Loader should NOT have been called @#" + i, loader.wasCalled()); 141 } 142 143 // wait for entries to expire, but don't call expireEntries 144 ticker.advance(EXPIRING_TIME * 10, MILLISECONDS); 145 146 // add a single unexpired entry 147 cache.getUnchecked(KEY_PREFIX + 11); 148 149 // collections views shouldn't expose expired entries 150 assertEquals(1, Iterators.size(cache.asMap().entrySet().iterator())); 151 assertEquals(1, Iterators.size(cache.asMap().keySet().iterator())); 152 assertEquals(1, Iterators.size(cache.asMap().values().iterator())); 153 154 CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); 155 156 for (int i = 0; i < 11; i++) { 157 assertFalse(cache.asMap().containsKey(KEY_PREFIX + i)); 158 } 159 assertEquals(11, removalListener.getCount()); 160 161 for (int i = 0; i < 10; i++) { 162 assertFalse(cache.asMap().containsKey(KEY_PREFIX + i)); 163 loader.reset(); 164 assertEquals(Integer.valueOf(VALUE_PREFIX + i), cache.getUnchecked(KEY_PREFIX + i)); 165 assertTrue("Creator should have been called @#" + i, loader.wasCalled()); 166 } 167 168 // expire new values we just created 169 CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); 170 assertEquals("Eviction notifications must be received", 21, removalListener.getCount()); 171 172 CacheTesting.expireEntries((LoadingCache<?, ?>) cache, EXPIRING_TIME, ticker); 173 // ensure that no new notifications are sent 174 assertEquals("Eviction notifications must be received", 21, removalListener.getCount()); 175 } 176 testRemovalListener_expireAfterWrite()177 public void testRemovalListener_expireAfterWrite() { 178 FakeTicker ticker = new FakeTicker(); 179 final AtomicInteger evictionCount = new AtomicInteger(); 180 final AtomicInteger applyCount = new AtomicInteger(); 181 final AtomicInteger totalSum = new AtomicInteger(); 182 183 RemovalListener<Integer, AtomicInteger> removalListener = 184 new RemovalListener<Integer, AtomicInteger>() { 185 @Override 186 public void onRemoval(RemovalNotification<Integer, AtomicInteger> notification) { 187 if (notification.wasEvicted()) { 188 evictionCount.incrementAndGet(); 189 totalSum.addAndGet(notification.getValue().get()); 190 } 191 } 192 }; 193 194 CacheLoader<Integer, AtomicInteger> loader = 195 new CacheLoader<Integer, AtomicInteger>() { 196 @Override 197 public AtomicInteger load(Integer key) { 198 applyCount.incrementAndGet(); 199 return new AtomicInteger(); 200 } 201 }; 202 203 LoadingCache<Integer, AtomicInteger> cache = 204 CacheBuilder.newBuilder() 205 .removalListener(removalListener) 206 .expireAfterWrite(10, MILLISECONDS) 207 .ticker(ticker) 208 .build(loader); 209 210 // Increment 100 times 211 for (int i = 0; i < 100; ++i) { 212 cache.getUnchecked(10).incrementAndGet(); 213 ticker.advance(1, MILLISECONDS); 214 } 215 216 assertEquals(evictionCount.get() + 1, applyCount.get()); 217 int remaining = cache.getUnchecked(10).get(); 218 assertEquals(100, totalSum.get() + remaining); 219 } 220 testRemovalScheduler_expireAfterWrite()221 public void testRemovalScheduler_expireAfterWrite() { 222 FakeTicker ticker = new FakeTicker(); 223 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 224 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 225 LoadingCache<String, Integer> cache = 226 CacheBuilder.newBuilder() 227 .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) 228 .removalListener(removalListener) 229 .ticker(ticker) 230 .build(loader); 231 runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME); 232 } 233 testRemovalScheduler_expireAfterAccess()234 public void testRemovalScheduler_expireAfterAccess() { 235 FakeTicker ticker = new FakeTicker(); 236 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 237 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 238 LoadingCache<String, Integer> cache = 239 CacheBuilder.newBuilder() 240 .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) 241 .removalListener(removalListener) 242 .ticker(ticker) 243 .build(loader); 244 runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME); 245 } 246 testRemovalScheduler_expireAfterBoth()247 public void testRemovalScheduler_expireAfterBoth() { 248 FakeTicker ticker = new FakeTicker(); 249 CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); 250 WatchedCreatorLoader loader = new WatchedCreatorLoader(); 251 LoadingCache<String, Integer> cache = 252 CacheBuilder.newBuilder() 253 .expireAfterAccess(EXPIRING_TIME, MILLISECONDS) 254 .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) 255 .removalListener(removalListener) 256 .ticker(ticker) 257 .build(loader); 258 runRemovalScheduler(cache, removalListener, loader, ticker, KEY_PREFIX, EXPIRING_TIME); 259 } 260 testExpirationOrder_access()261 public void testExpirationOrder_access() { 262 // test lru within a single segment 263 FakeTicker ticker = new FakeTicker(); 264 IdentityLoader<Integer> loader = identityLoader(); 265 LoadingCache<Integer, Integer> cache = 266 CacheBuilder.newBuilder() 267 .concurrencyLevel(1) 268 .expireAfterAccess(11, MILLISECONDS) 269 .ticker(ticker) 270 .build(loader); 271 for (int i = 0; i < 10; i++) { 272 cache.getUnchecked(i); 273 ticker.advance(1, MILLISECONDS); 274 } 275 Set<Integer> keySet = cache.asMap().keySet(); 276 assertThat(keySet).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); 277 278 // 0 expires 279 ticker.advance(1, MILLISECONDS); 280 assertThat(keySet).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); 281 282 // reorder 283 getAll(cache, asList(0, 1, 2)); 284 CacheTesting.drainRecencyQueues(cache); 285 ticker.advance(2, MILLISECONDS); 286 assertThat(keySet).containsExactly(3, 4, 5, 6, 7, 8, 9, 0, 1, 2); 287 288 // 3 expires 289 ticker.advance(1, MILLISECONDS); 290 assertThat(keySet).containsExactly(4, 5, 6, 7, 8, 9, 0, 1, 2); 291 292 // reorder 293 getAll(cache, asList(5, 7, 9)); 294 CacheTesting.drainRecencyQueues(cache); 295 assertThat(keySet).containsExactly(4, 6, 8, 0, 1, 2, 5, 7, 9); 296 297 // 4 expires 298 ticker.advance(1, MILLISECONDS); 299 assertThat(keySet).containsExactly(6, 8, 0, 1, 2, 5, 7, 9); 300 ticker.advance(1, MILLISECONDS); 301 assertThat(keySet).containsExactly(6, 8, 0, 1, 2, 5, 7, 9); 302 303 // 6 expires 304 ticker.advance(1, MILLISECONDS); 305 assertThat(keySet).containsExactly(8, 0, 1, 2, 5, 7, 9); 306 ticker.advance(1, MILLISECONDS); 307 assertThat(keySet).containsExactly(8, 0, 1, 2, 5, 7, 9); 308 309 // 8 expires 310 ticker.advance(1, MILLISECONDS); 311 assertThat(keySet).containsExactly(0, 1, 2, 5, 7, 9); 312 } 313 testExpirationOrder_write()314 public void testExpirationOrder_write() throws ExecutionException { 315 // test lru within a single segment 316 FakeTicker ticker = new FakeTicker(); 317 IdentityLoader<Integer> loader = identityLoader(); 318 LoadingCache<Integer, Integer> cache = 319 CacheBuilder.newBuilder() 320 .concurrencyLevel(1) 321 .expireAfterWrite(11, MILLISECONDS) 322 .ticker(ticker) 323 .build(loader); 324 for (int i = 0; i < 10; i++) { 325 cache.getUnchecked(i); 326 ticker.advance(1, MILLISECONDS); 327 } 328 Set<Integer> keySet = cache.asMap().keySet(); 329 assertThat(keySet).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); 330 331 // 0 expires 332 ticker.advance(1, MILLISECONDS); 333 assertThat(keySet).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9); 334 335 // get doesn't stop 1 from expiring 336 getAll(cache, asList(0, 1, 2)); 337 CacheTesting.drainRecencyQueues(cache); 338 ticker.advance(1, MILLISECONDS); 339 assertThat(keySet).containsExactly(2, 3, 4, 5, 6, 7, 8, 9, 0); 340 341 // get(K, Callable) doesn't stop 2 from expiring 342 cache.get(2, Callables.returning(-2)); 343 CacheTesting.drainRecencyQueues(cache); 344 ticker.advance(1, MILLISECONDS); 345 assertThat(keySet).containsExactly(3, 4, 5, 6, 7, 8, 9, 0); 346 347 // asMap.put saves 3 348 cache.asMap().put(3, -3); 349 ticker.advance(1, MILLISECONDS); 350 assertThat(keySet).containsExactly(4, 5, 6, 7, 8, 9, 0, 3); 351 352 // asMap.replace saves 4 353 cache.asMap().replace(4, -4); 354 ticker.advance(1, MILLISECONDS); 355 assertThat(keySet).containsExactly(5, 6, 7, 8, 9, 0, 3, 4); 356 357 // 5 expires 358 ticker.advance(1, MILLISECONDS); 359 assertThat(keySet).containsExactly(6, 7, 8, 9, 0, 3, 4); 360 } 361 testExpirationOrder_writeAccess()362 public void testExpirationOrder_writeAccess() throws ExecutionException { 363 // test lru within a single segment 364 FakeTicker ticker = new FakeTicker(); 365 IdentityLoader<Integer> loader = identityLoader(); 366 LoadingCache<Integer, Integer> cache = 367 CacheBuilder.newBuilder() 368 .concurrencyLevel(1) 369 .expireAfterWrite(5, MILLISECONDS) 370 .expireAfterAccess(3, MILLISECONDS) 371 .ticker(ticker) 372 .build(loader); 373 for (int i = 0; i < 5; i++) { 374 cache.getUnchecked(i); 375 } 376 ticker.advance(1, MILLISECONDS); 377 for (int i = 5; i < 10; i++) { 378 cache.getUnchecked(i); 379 } 380 ticker.advance(1, MILLISECONDS); 381 382 Set<Integer> keySet = cache.asMap().keySet(); 383 assertThat(keySet).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); 384 385 // get saves 1, 3; 0, 2, 4 expire 386 getAll(cache, asList(1, 3)); 387 CacheTesting.drainRecencyQueues(cache); 388 ticker.advance(1, MILLISECONDS); 389 assertThat(keySet).containsExactly(5, 6, 7, 8, 9, 1, 3); 390 391 // get saves 6, 8; 5, 7, 9 expire 392 getAll(cache, asList(6, 8)); 393 CacheTesting.drainRecencyQueues(cache); 394 ticker.advance(1, MILLISECONDS); 395 assertThat(keySet).containsExactly(1, 3, 6, 8); 396 397 // get fails to save 1, put saves 3 398 cache.asMap().put(3, -3); 399 getAll(cache, asList(1)); 400 CacheTesting.drainRecencyQueues(cache); 401 ticker.advance(1, MILLISECONDS); 402 assertThat(keySet).containsExactly(6, 8, 3); 403 404 // get(K, Callable) fails to save 8, replace saves 6 405 cache.asMap().replace(6, -6); 406 cache.get(8, Callables.returning(-8)); 407 CacheTesting.drainRecencyQueues(cache); 408 ticker.advance(1, MILLISECONDS); 409 assertThat(keySet).containsExactly(3, 6); 410 } 411 testExpiration_invalidateAll()412 public void testExpiration_invalidateAll() { 413 FakeTicker ticker = new FakeTicker(); 414 QueuingRemovalListener<Integer, Integer> listener = 415 TestingRemovalListeners.queuingRemovalListener(); 416 Cache<Integer, Integer> cache = 417 CacheBuilder.newBuilder() 418 .expireAfterAccess(1, TimeUnit.MINUTES) 419 .removalListener(listener) 420 .ticker(ticker) 421 .build(); 422 cache.put(1, 1); 423 ticker.advance(10, TimeUnit.MINUTES); 424 cache.invalidateAll(); 425 426 assertThat(listener.poll().getCause()).isEqualTo(RemovalCause.EXPIRED); 427 } 428 runRemovalScheduler( LoadingCache<String, Integer> cache, CountingRemovalListener<String, Integer> removalListener, WatchedCreatorLoader loader, FakeTicker ticker, String keyPrefix, long ttl)429 private void runRemovalScheduler( 430 LoadingCache<String, Integer> cache, 431 CountingRemovalListener<String, Integer> removalListener, 432 WatchedCreatorLoader loader, 433 FakeTicker ticker, 434 String keyPrefix, 435 long ttl) { 436 437 int shift1 = 10 + VALUE_PREFIX; 438 loader.setValuePrefix(shift1); 439 // fill with initial data 440 for (int i = 0; i < 10; i++) { 441 assertEquals(Integer.valueOf(i + shift1), cache.getUnchecked(keyPrefix + i)); 442 } 443 assertEquals(10, CacheTesting.expirationQueueSize(cache)); 444 assertEquals(0, removalListener.getCount()); 445 446 // wait, so that entries have just 10 ms to live 447 ticker.advance(ttl * 2 / 3, MILLISECONDS); 448 449 assertEquals(10, CacheTesting.expirationQueueSize(cache)); 450 assertEquals(0, removalListener.getCount()); 451 452 int shift2 = shift1 + 10; 453 loader.setValuePrefix(shift2); 454 // fill with new data - has to live for 20 ms more 455 for (int i = 0; i < 10; i++) { 456 cache.invalidate(keyPrefix + i); 457 assertEquals( 458 "key: " + keyPrefix + i, Integer.valueOf(i + shift2), cache.getUnchecked(keyPrefix + i)); 459 } 460 assertEquals(10, CacheTesting.expirationQueueSize(cache)); 461 assertEquals(10, removalListener.getCount()); // these are the invalidated ones 462 463 // old timeouts must expire after this wait 464 ticker.advance(ttl * 2 / 3, MILLISECONDS); 465 466 assertEquals(10, CacheTesting.expirationQueueSize(cache)); 467 assertEquals(10, removalListener.getCount()); 468 469 // check that new values are still there - they still have 10 ms to live 470 for (int i = 0; i < 10; i++) { 471 loader.reset(); 472 assertEquals(Integer.valueOf(i + shift2), cache.getUnchecked(keyPrefix + i)); 473 assertFalse("Creator should NOT have been called @#" + i, loader.wasCalled()); 474 } 475 assertEquals(10, removalListener.getCount()); 476 } 477 getAll(LoadingCache<Integer, Integer> cache, List<Integer> keys)478 private static void getAll(LoadingCache<Integer, Integer> cache, List<Integer> keys) { 479 for (int i : keys) { 480 cache.getUnchecked(i); 481 } 482 } 483 484 private static class WatchedCreatorLoader extends CacheLoader<String, Integer> { 485 boolean wasCalled = false; // must be set in load() 486 String keyPrefix = KEY_PREFIX; 487 int valuePrefix = VALUE_PREFIX; 488 WatchedCreatorLoader()489 public WatchedCreatorLoader() {} 490 reset()491 public void reset() { 492 wasCalled = false; 493 } 494 wasCalled()495 public boolean wasCalled() { 496 return wasCalled; 497 } 498 setKeyPrefix(String keyPrefix)499 public void setKeyPrefix(String keyPrefix) { 500 this.keyPrefix = keyPrefix; 501 } 502 setValuePrefix(int valuePrefix)503 public void setValuePrefix(int valuePrefix) { 504 this.valuePrefix = valuePrefix; 505 } 506 507 @Override load(String key)508 public Integer load(String key) { 509 wasCalled = true; 510 return valuePrefix + Integer.parseInt(key.substring(keyPrefix.length())); 511 } 512 } 513 } 514