• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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