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