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.base.Preconditions.checkNotNull; 18 import static com.google.common.truth.Truth.assertThat; 19 import static junit.framework.Assert.assertEquals; 20 import static junit.framework.Assert.assertFalse; 21 import static junit.framework.Assert.assertNotNull; 22 import static junit.framework.Assert.assertNotSame; 23 import static junit.framework.Assert.assertNull; 24 import static junit.framework.Assert.assertSame; 25 import static junit.framework.Assert.assertTrue; 26 27 import com.google.common.base.Preconditions; 28 import com.google.common.cache.LocalCache.LocalLoadingCache; 29 import com.google.common.cache.LocalCache.Segment; 30 import com.google.common.cache.LocalCache.ValueReference; 31 import com.google.common.collect.ImmutableList; 32 import com.google.common.collect.ImmutableMap; 33 import com.google.common.collect.ImmutableSet; 34 import com.google.common.collect.Maps; 35 import com.google.common.collect.Sets; 36 import com.google.common.testing.EqualsTester; 37 import com.google.common.testing.FakeTicker; 38 import java.lang.ref.Reference; 39 import java.util.Collection; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 import java.util.Set; 44 import java.util.concurrent.ConcurrentMap; 45 import java.util.concurrent.TimeUnit; 46 import java.util.concurrent.atomic.AtomicReferenceArray; 47 import org.checkerframework.checker.nullness.qual.Nullable; 48 49 /** 50 * A collection of utilities for {@link Cache} testing. 51 * 52 * @author mike nonemacher 53 */ 54 @SuppressWarnings("GuardedBy") // TODO(b/35466881): Fix or suppress. 55 class CacheTesting { 56 57 /** 58 * Poke into the Cache internals to simulate garbage collection of the value associated with the 59 * given key. This assumes that the associated entry is a WeakValueReference or a 60 * SoftValueReference (and not a LoadingValueReference), and throws an IllegalStateException if 61 * that assumption does not hold. 62 */ 63 @SuppressWarnings("unchecked") // the instanceof check and the cast generate this warning simulateValueReclamation(Cache<K, V> cache, K key)64 static <K, V> void simulateValueReclamation(Cache<K, V> cache, K key) { 65 ReferenceEntry<K, V> entry = getReferenceEntry(cache, key); 66 if (entry != null) { 67 ValueReference<K, V> valueRef = entry.getValueReference(); 68 // fail on strong/computing refs 69 Preconditions.checkState(valueRef instanceof Reference); 70 Reference<V> ref = (Reference<V>) valueRef; 71 if (ref != null) { 72 ref.clear(); 73 } 74 } 75 } 76 77 /** 78 * Poke into the Cache internals to simulate garbage collection of the given key. This assumes 79 * that the given entry is a weak or soft reference, and throws an IllegalStateException if that 80 * assumption does not hold. 81 */ simulateKeyReclamation(Cache<K, V> cache, K key)82 static <K, V> void simulateKeyReclamation(Cache<K, V> cache, K key) { 83 ReferenceEntry<K, V> entry = getReferenceEntry(cache, key); 84 85 Preconditions.checkState(entry instanceof Reference); 86 Reference<?> ref = (Reference<?>) entry; 87 if (ref != null) { 88 ref.clear(); 89 } 90 } 91 getReferenceEntry(Cache<K, V> cache, K key)92 static <K, V> ReferenceEntry<K, V> getReferenceEntry(Cache<K, V> cache, K key) { 93 checkNotNull(cache); 94 checkNotNull(key); 95 LocalCache<K, V> map = toLocalCache(cache); 96 return map.getEntry(key); 97 } 98 99 /** 100 * Forces the segment containing the given {@code key} to expand (see {@link Segment#expand()}). 101 */ forceExpandSegment(Cache<K, V> cache, K key)102 static <K, V> void forceExpandSegment(Cache<K, V> cache, K key) { 103 checkNotNull(cache); 104 checkNotNull(key); 105 LocalCache<K, V> map = toLocalCache(cache); 106 int hash = map.hash(key); 107 Segment<K, V> segment = map.segmentFor(hash); 108 segment.expand(); 109 } 110 111 /** 112 * Gets the {@link LocalCache} used by the given {@link Cache}, if any, or throws an 113 * IllegalArgumentException if this is a Cache type that doesn't have a LocalCache. 114 */ toLocalCache(Cache<K, V> cache)115 static <K, V> LocalCache<K, V> toLocalCache(Cache<K, V> cache) { 116 if (cache instanceof LocalLoadingCache) { 117 return ((LocalLoadingCache<K, V>) cache).localCache; 118 } 119 throw new IllegalArgumentException( 120 "Cache of type " + cache.getClass() + " doesn't have a LocalCache."); 121 } 122 123 /** 124 * Determines whether the given cache can be converted to a LocalCache by {@link #toLocalCache} 125 * without throwing an exception. 126 */ hasLocalCache(Cache<?, ?> cache)127 static boolean hasLocalCache(Cache<?, ?> cache) { 128 return (checkNotNull(cache) instanceof LocalLoadingCache); 129 } 130 drainRecencyQueues(Cache<?, ?> cache)131 static void drainRecencyQueues(Cache<?, ?> cache) { 132 if (hasLocalCache(cache)) { 133 LocalCache<?, ?> map = toLocalCache(cache); 134 for (Segment<?, ?> segment : map.segments) { 135 drainRecencyQueue(segment); 136 } 137 } 138 } 139 drainRecencyQueue(Segment<?, ?> segment)140 static void drainRecencyQueue(Segment<?, ?> segment) { 141 segment.lock(); 142 try { 143 segment.cleanUp(); 144 } finally { 145 segment.unlock(); 146 } 147 } 148 drainReferenceQueues(Cache<?, ?> cache)149 static void drainReferenceQueues(Cache<?, ?> cache) { 150 if (hasLocalCache(cache)) { 151 drainReferenceQueues(toLocalCache(cache)); 152 } 153 } 154 drainReferenceQueues(LocalCache<?, ?> cchm)155 static void drainReferenceQueues(LocalCache<?, ?> cchm) { 156 for (LocalCache.Segment<?, ?> segment : cchm.segments) { 157 drainReferenceQueue(segment); 158 } 159 } 160 drainReferenceQueue(LocalCache.Segment<?, ?> segment)161 static void drainReferenceQueue(LocalCache.Segment<?, ?> segment) { 162 segment.lock(); 163 try { 164 segment.drainReferenceQueues(); 165 } finally { 166 segment.unlock(); 167 } 168 } 169 getTotalSegmentSize(Cache<?, ?> cache)170 static int getTotalSegmentSize(Cache<?, ?> cache) { 171 LocalCache<?, ?> map = toLocalCache(cache); 172 int totalSize = 0; 173 for (Segment<?, ?> segment : map.segments) { 174 totalSize += segment.maxSegmentWeight; 175 } 176 return totalSize; 177 } 178 179 /** 180 * Peeks into the cache's internals to check its internal consistency. Verifies that each 181 * segment's count matches its #elements (after cleanup), each segment is unlocked, each entry 182 * contains a non-null key and value, and the eviction and expiration queues are consistent (see 183 * {@link #checkEviction}, {@link #checkExpiration}). 184 */ checkValidState(Cache<?, ?> cache)185 static void checkValidState(Cache<?, ?> cache) { 186 if (hasLocalCache(cache)) { 187 checkValidState(toLocalCache(cache)); 188 } 189 } 190 checkValidState(LocalCache<?, ?> cchm)191 static void checkValidState(LocalCache<?, ?> cchm) { 192 for (Segment<?, ?> segment : cchm.segments) { 193 segment.cleanUp(); 194 assertFalse(segment.isLocked()); 195 Map<?, ?> table = segmentTable(segment); 196 // cleanup and then check count after we have a strong reference to all entries 197 segment.cleanUp(); 198 // under high memory pressure keys/values may be nulled out but not yet enqueued 199 assertThat(table.size()).isAtMost(segment.count); 200 for (Entry<?, ?> entry : table.entrySet()) { 201 assertNotNull(entry.getKey()); 202 assertNotNull(entry.getValue()); 203 assertSame(entry.getValue(), cchm.get(entry.getKey())); 204 } 205 } 206 checkEviction(cchm); 207 checkExpiration(cchm); 208 } 209 210 /** 211 * Peeks into the cache's internals to verify that its expiration queue is consistent. Verifies 212 * that the next/prev links in the expiration queue are correct, and that the queue is ordered by 213 * expiration time. 214 */ checkExpiration(Cache<?, ?> cache)215 static void checkExpiration(Cache<?, ?> cache) { 216 if (hasLocalCache(cache)) { 217 checkExpiration(toLocalCache(cache)); 218 } 219 } 220 checkExpiration(LocalCache<?, ?> cchm)221 static void checkExpiration(LocalCache<?, ?> cchm) { 222 for (Segment<?, ?> segment : cchm.segments) { 223 if (cchm.usesWriteQueue()) { 224 Set<ReferenceEntry<?, ?>> entries = Sets.newIdentityHashSet(); 225 226 ReferenceEntry<?, ?> prev = null; 227 for (ReferenceEntry<?, ?> current : segment.writeQueue) { 228 assertTrue(entries.add(current)); 229 if (prev != null) { 230 assertSame(prev, current.getPreviousInWriteQueue()); 231 assertSame(prev.getNextInWriteQueue(), current); 232 assertThat(prev.getWriteTime()).isAtMost(current.getWriteTime()); 233 } 234 Object key = current.getKey(); 235 if (key != null) { 236 assertSame(current, segment.getEntry(key, current.getHash())); 237 } 238 prev = current; 239 } 240 assertEquals(segment.count, entries.size()); 241 } else { 242 assertTrue(segment.writeQueue.isEmpty()); 243 } 244 245 if (cchm.usesAccessQueue()) { 246 Set<ReferenceEntry<?, ?>> entries = Sets.newIdentityHashSet(); 247 248 ReferenceEntry<?, ?> prev = null; 249 for (ReferenceEntry<?, ?> current : segment.accessQueue) { 250 assertTrue(entries.add(current)); 251 if (prev != null) { 252 assertSame(prev, current.getPreviousInAccessQueue()); 253 assertSame(prev.getNextInAccessQueue(), current); 254 // read accesses may be slightly misordered 255 assertTrue( 256 prev.getAccessTime() <= current.getAccessTime() 257 || prev.getAccessTime() - current.getAccessTime() < 1000); 258 } 259 Object key = current.getKey(); 260 if (key != null) { 261 assertSame(current, segment.getEntry(key, current.getHash())); 262 } 263 prev = current; 264 } 265 assertEquals(segment.count, entries.size()); 266 } else { 267 assertTrue(segment.accessQueue.isEmpty()); 268 } 269 } 270 } 271 272 /** 273 * Peeks into the cache's internals to verify that its eviction queue is consistent. Verifies that 274 * the prev/next links are correct, and that all items in each segment are also in that segment's 275 * eviction (recency) queue. 276 */ 277 static void checkEviction(Cache<?, ?> cache) { 278 if (hasLocalCache(cache)) { 279 checkEviction(toLocalCache(cache)); 280 } 281 } 282 283 static void checkEviction(LocalCache<?, ?> map) { 284 if (map.evictsBySize()) { 285 for (Segment<?, ?> segment : map.segments) { 286 drainRecencyQueue(segment); 287 assertEquals(0, segment.recencyQueue.size()); 288 assertEquals(0, segment.readCount.get()); 289 290 ReferenceEntry<?, ?> prev = null; 291 for (ReferenceEntry<?, ?> current : segment.accessQueue) { 292 if (prev != null) { 293 assertSame(prev, current.getPreviousInAccessQueue()); 294 assertSame(prev.getNextInAccessQueue(), current); 295 } 296 Object key = current.getKey(); 297 if (key != null) { 298 assertSame(current, segment.getEntry(key, current.getHash())); 299 } 300 prev = current; 301 } 302 } 303 } else { 304 for (Segment<?, ?> segment : map.segments) { 305 assertEquals(0, segment.recencyQueue.size()); 306 } 307 } 308 } 309 310 static int segmentSize(Segment<?, ?> segment) { 311 Map<?, ?> map = segmentTable(segment); 312 return map.size(); 313 } 314 315 static <K, V> Map<K, V> segmentTable(Segment<K, V> segment) { 316 AtomicReferenceArray<? extends ReferenceEntry<K, V>> table = segment.table; 317 Map<K, V> map = Maps.newLinkedHashMap(); 318 for (int i = 0; i < table.length(); i++) { 319 for (ReferenceEntry<K, V> entry = table.get(i); entry != null; entry = entry.getNext()) { 320 K key = entry.getKey(); 321 V value = entry.getValueReference().get(); 322 if (key != null && value != null) { 323 assertNull(map.put(key, value)); 324 } 325 } 326 } 327 return map; 328 } 329 330 static int writeQueueSize(Cache<?, ?> cache) { 331 LocalCache<?, ?> cchm = toLocalCache(cache); 332 333 int size = 0; 334 for (Segment<?, ?> segment : cchm.segments) { 335 size += writeQueueSize(segment); 336 } 337 return size; 338 } 339 340 static int writeQueueSize(Segment<?, ?> segment) { 341 return segment.writeQueue.size(); 342 } 343 344 static int accessQueueSize(Cache<?, ?> cache) { 345 LocalCache<?, ?> cchm = toLocalCache(cache); 346 int size = 0; 347 for (Segment<?, ?> segment : cchm.segments) { 348 size += accessQueueSize(segment); 349 } 350 return size; 351 } 352 353 static int accessQueueSize(Segment<?, ?> segment) { 354 return segment.accessQueue.size(); 355 } 356 357 static int expirationQueueSize(Cache<?, ?> cache) { 358 return Math.max(accessQueueSize(cache), writeQueueSize(cache)); 359 } 360 361 static void processPendingNotifications(Cache<?, ?> cache) { 362 if (hasLocalCache(cache)) { 363 LocalCache<?, ?> cchm = toLocalCache(cache); 364 cchm.processPendingNotifications(); 365 } 366 } 367 368 interface Receiver<T> { 369 void accept(@Nullable T object); 370 } 371 372 /** 373 * Assuming the given cache has maximum size {@code maxSize}, this method populates the cache (by 374 * getting a bunch of different keys), then makes sure all the items in the cache are also in the 375 * eviction queue. It will invoke the given {@code operation} on the first element in the eviction 376 * queue, and then reverify that all items in the cache are in the eviction queue, and verify that 377 * the head of the eviction queue has changed as a result of the operation. 378 */ 379 static void checkRecency( 380 LoadingCache<Integer, Integer> cache, 381 int maxSize, 382 Receiver<ReferenceEntry<Integer, Integer>> operation) { 383 checkNotNull(operation); 384 if (hasLocalCache(cache)) { 385 warmUp(cache, 0, 2 * maxSize); 386 387 LocalCache<Integer, Integer> cchm = toLocalCache(cache); 388 Segment<?, ?> segment = cchm.segments[0]; 389 drainRecencyQueue(segment); 390 assertEquals(maxSize, accessQueueSize(cache)); 391 assertEquals(maxSize, cache.size()); 392 393 ReferenceEntry<?, ?> originalHead = segment.accessQueue.peek(); 394 @SuppressWarnings("unchecked") 395 ReferenceEntry<Integer, Integer> entry = (ReferenceEntry<Integer, Integer>) originalHead; 396 operation.accept(entry); 397 drainRecencyQueue(segment); 398 399 assertNotSame(originalHead, segment.accessQueue.peek()); 400 assertEquals(cache.size(), accessQueueSize(cache)); 401 } 402 } 403 404 /** Warms the given cache by getting all values in {@code [start, end)}, in order. */ 405 static void warmUp(LoadingCache<Integer, Integer> map, int start, int end) { 406 checkNotNull(map); 407 for (int i = start; i < end; i++) { 408 map.getUnchecked(i); 409 } 410 } 411 412 static void expireEntries(Cache<?, ?> cache, long expiringTime, FakeTicker ticker) { 413 checkNotNull(ticker); 414 expireEntries(toLocalCache(cache), expiringTime, ticker); 415 } 416 417 static void expireEntries(LocalCache<?, ?> cchm, long expiringTime, FakeTicker ticker) { 418 419 for (Segment<?, ?> segment : cchm.segments) { 420 drainRecencyQueue(segment); 421 } 422 423 ticker.advance(2 * expiringTime, TimeUnit.MILLISECONDS); 424 425 long now = ticker.read(); 426 for (Segment<?, ?> segment : cchm.segments) { 427 expireEntries(segment, now); 428 assertEquals("Expiration queue must be empty by now", 0, writeQueueSize(segment)); 429 assertEquals("Expiration queue must be empty by now", 0, accessQueueSize(segment)); 430 assertEquals("Segments must be empty by now", 0, segmentSize(segment)); 431 } 432 cchm.processPendingNotifications(); 433 } 434 435 static void expireEntries(Segment<?, ?> segment, long now) { 436 segment.lock(); 437 try { 438 segment.expireEntries(now); 439 segment.cleanUp(); 440 } finally { 441 segment.unlock(); 442 } 443 } 444 445 static void checkEmpty(Cache<?, ?> cache) { 446 assertEquals(0, cache.size()); 447 assertFalse(cache.asMap().containsKey(null)); 448 assertFalse(cache.asMap().containsKey(6)); 449 assertFalse(cache.asMap().containsValue(null)); 450 assertFalse(cache.asMap().containsValue(6)); 451 checkEmpty(cache.asMap()); 452 } 453 454 static void checkEmpty(ConcurrentMap<?, ?> map) { 455 checkEmpty(map.keySet()); 456 checkEmpty(map.values()); 457 checkEmpty(map.entrySet()); 458 assertEquals(ImmutableMap.of(), map); 459 assertEquals(ImmutableMap.of().hashCode(), map.hashCode()); 460 assertEquals(ImmutableMap.of().toString(), map.toString()); 461 462 if (map instanceof LocalCache) { 463 LocalCache<?, ?> cchm = (LocalCache<?, ?>) map; 464 465 checkValidState(cchm); 466 assertTrue(cchm.isEmpty()); 467 assertEquals(0, cchm.size()); 468 for (LocalCache.Segment<?, ?> segment : cchm.segments) { 469 assertEquals(0, segment.count); 470 assertEquals(0, segmentSize(segment)); 471 assertTrue(segment.writeQueue.isEmpty()); 472 assertTrue(segment.accessQueue.isEmpty()); 473 } 474 } 475 } 476 477 static void checkEmpty(Collection<?> collection) { 478 assertTrue(collection.isEmpty()); 479 assertEquals(0, collection.size()); 480 assertFalse(collection.iterator().hasNext()); 481 assertThat(collection.toArray()).isEmpty(); 482 assertThat(collection.toArray(new Object[0])).isEmpty(); 483 if (collection instanceof Set) { 484 new EqualsTester() 485 .addEqualityGroup(ImmutableSet.of(), collection) 486 .addEqualityGroup(ImmutableSet.of("")) 487 .testEquals(); 488 } else if (collection instanceof List) { 489 new EqualsTester() 490 .addEqualityGroup(ImmutableList.of(), collection) 491 .addEqualityGroup(ImmutableList.of("")) 492 .testEquals(); 493 } 494 } 495 } 496