• 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.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