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