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