1 /*
2  * Copyright (C) 2011 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.cache;
18 
19 import static com.google.common.cache.CacheBuilder.EMPTY_STATS;
20 import static com.google.common.cache.LocalCacheTest.SMALL_MAX_SIZE;
21 import static com.google.common.cache.TestingCacheLoaders.identityLoader;
22 import static com.google.common.truth.Truth.assertThat;
23 
24 import com.google.common.cache.LocalCache.LocalLoadingCache;
25 import com.google.common.cache.LocalCache.Segment;
26 import com.google.common.collect.ImmutableMap;
27 import com.google.common.collect.ImmutableSet;
28 import com.google.common.testing.NullPointerTester;
29 
30 import junit.framework.TestCase;
31 
32 import java.lang.Thread.UncaughtExceptionHandler;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.concurrent.ConcurrentMap;
36 import java.util.concurrent.CountDownLatch;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.atomic.AtomicReference;
39 
40 /**
41  * @author Charles Fry
42  */
43 public class LocalLoadingCacheTest extends TestCase {
44 
makeCache( CacheBuilder<K, V> builder, CacheLoader<? super K, V> loader)45   private static <K, V> LocalLoadingCache<K, V> makeCache(
46       CacheBuilder<K, V> builder, CacheLoader<? super K, V> loader) {
47     return new LocalLoadingCache<K, V>(builder, loader);
48   }
49 
createCacheBuilder()50   private CacheBuilder<Object, Object> createCacheBuilder() {
51     return CacheBuilder.newBuilder().recordStats();
52   }
53 
54   // constructor tests
55 
testComputingFunction()56   public void testComputingFunction() {
57     CacheLoader<Object, Object> loader = new CacheLoader<Object, Object>() {
58       @Override
59       public Object load(Object from) {
60         return new Object();
61       }
62     };
63     LocalLoadingCache<Object, Object> cache = makeCache(createCacheBuilder(), loader);
64     assertSame(loader, cache.localCache.defaultLoader);
65   }
66 
67   // null parameters test
68 
testNullParameters()69   public void testNullParameters() throws Exception {
70     NullPointerTester tester = new NullPointerTester();
71     CacheLoader<Object, Object> loader = identityLoader();
72     tester.testAllPublicInstanceMethods(makeCache(createCacheBuilder(), loader));
73   }
74 
75   // stats tests
76 
testStats()77   public void testStats() {
78     CacheBuilder<Object, Object> builder = createCacheBuilder()
79         .concurrencyLevel(1)
80         .maximumSize(2);
81     LocalLoadingCache<Object, Object> cache = makeCache(builder, identityLoader());
82     assertEquals(EMPTY_STATS, cache.stats());
83 
84     Object one = new Object();
85     cache.getUnchecked(one);
86     CacheStats stats = cache.stats();
87     assertEquals(1, stats.requestCount());
88     assertEquals(0, stats.hitCount());
89     assertEquals(0.0, stats.hitRate());
90     assertEquals(1, stats.missCount());
91     assertEquals(1.0, stats.missRate());
92     assertEquals(1, stats.loadCount());
93     long totalLoadTime = stats.totalLoadTime();
94     assertTrue(totalLoadTime >= 0);
95     assertTrue(stats.averageLoadPenalty() >= 0.0);
96     assertEquals(0, stats.evictionCount());
97 
98     cache.getUnchecked(one);
99     stats = cache.stats();
100     assertEquals(2, stats.requestCount());
101     assertEquals(1, stats.hitCount());
102     assertEquals(1.0/2, stats.hitRate());
103     assertEquals(1, stats.missCount());
104     assertEquals(1.0/2, stats.missRate());
105     assertEquals(1, stats.loadCount());
106     assertEquals(0, stats.evictionCount());
107 
108     Object two = new Object();
109     cache.getUnchecked(two);
110     stats = cache.stats();
111     assertEquals(3, stats.requestCount());
112     assertEquals(1, stats.hitCount());
113     assertEquals(1.0/3, stats.hitRate());
114     assertEquals(2, stats.missCount());
115     assertEquals(2.0/3, stats.missRate());
116     assertEquals(2, stats.loadCount());
117     assertTrue(stats.totalLoadTime() >= totalLoadTime);
118     totalLoadTime = stats.totalLoadTime();
119     assertTrue(stats.averageLoadPenalty() >= 0.0);
120     assertEquals(0, stats.evictionCount());
121 
122     Object three = new Object();
123     cache.getUnchecked(three);
124     stats = cache.stats();
125     assertEquals(4, stats.requestCount());
126     assertEquals(1, stats.hitCount());
127     assertEquals(1.0/4, stats.hitRate());
128     assertEquals(3, stats.missCount());
129     assertEquals(3.0/4, stats.missRate());
130     assertEquals(3, stats.loadCount());
131     assertTrue(stats.totalLoadTime() >= totalLoadTime);
132     totalLoadTime = stats.totalLoadTime();
133     assertTrue(stats.averageLoadPenalty() >= 0.0);
134     assertEquals(1, stats.evictionCount());
135   }
136 
testStatsNoops()137   public void testStatsNoops() {
138     CacheBuilder<Object, Object> builder = createCacheBuilder()
139         .concurrencyLevel(1);
140     LocalLoadingCache<Object, Object> cache = makeCache(builder, identityLoader());
141     ConcurrentMap<Object, Object> map = cache.localCache; // modifiable map view
142     assertEquals(EMPTY_STATS, cache.stats());
143 
144     Object one = new Object();
145     assertNull(map.put(one, one));
146     assertSame(one, map.get(one));
147     assertTrue(map.containsKey(one));
148     assertTrue(map.containsValue(one));
149     Object two = new Object();
150     assertSame(one, map.replace(one, two));
151     assertTrue(map.containsKey(one));
152     assertFalse(map.containsValue(one));
153     Object three = new Object();
154     assertTrue(map.replace(one, two, three));
155     assertTrue(map.remove(one, three));
156     assertFalse(map.containsKey(one));
157     assertFalse(map.containsValue(one));
158     assertNull(map.putIfAbsent(two, three));
159     assertSame(three, map.remove(two));
160     assertNull(map.put(three, one));
161     assertNull(map.put(one, two));
162 
163     assertThat(map).hasKey(three).withValue(one);
164     assertThat(map).hasKey(one).withValue(two);
165 
166     //TODO(user): Confirm with fry@ that this is a reasonable substitute.
167     //Set<Map.Entry<Object, Object>> entries = map.entrySet();
168     //assertThat(entries).has().exactly(
169     //    Maps.immutableEntry(three, one), Maps.immutableEntry(one, two));
170     //Set<Object> keys = map.keySet();
171     //assertThat(keys).has().exactly(one, three);
172     //Collection<Object> values = map.values();
173     //assertThat(values).has().exactly(one, two);
174 
175     map.clear();
176 
177     assertEquals(EMPTY_STATS, cache.stats());
178   }
179 
testNoStats()180   public void testNoStats() {
181     CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder()
182         .concurrencyLevel(1)
183         .maximumSize(2);
184     LocalLoadingCache<Object, Object> cache = makeCache(builder, identityLoader());
185     assertEquals(EMPTY_STATS, cache.stats());
186 
187     Object one = new Object();
188     cache.getUnchecked(one);
189     assertEquals(EMPTY_STATS, cache.stats());
190 
191     cache.getUnchecked(one);
192     assertEquals(EMPTY_STATS, cache.stats());
193 
194     Object two = new Object();
195     cache.getUnchecked(two);
196     assertEquals(EMPTY_STATS, cache.stats());
197 
198     Object three = new Object();
199     cache.getUnchecked(three);
200     assertEquals(EMPTY_STATS, cache.stats());
201   }
202 
testRecordStats()203   public void testRecordStats() {
204     CacheBuilder<Object, Object> builder = createCacheBuilder()
205         .recordStats()
206         .concurrencyLevel(1)
207         .maximumSize(2);
208     LocalLoadingCache<Object, Object> cache = makeCache(builder, identityLoader());
209     assertEquals(0, cache.stats().hitCount());
210     assertEquals(0, cache.stats().missCount());
211 
212     Object one = new Object();
213     cache.getUnchecked(one);
214     assertEquals(0, cache.stats().hitCount());
215     assertEquals(1, cache.stats().missCount());
216 
217     cache.getUnchecked(one);
218     assertEquals(1, cache.stats().hitCount());
219     assertEquals(1, cache.stats().missCount());
220 
221     Object two = new Object();
222     cache.getUnchecked(two);
223     assertEquals(1, cache.stats().hitCount());
224     assertEquals(2, cache.stats().missCount());
225 
226     Object three = new Object();
227     cache.getUnchecked(three);
228     assertEquals(1, cache.stats().hitCount());
229     assertEquals(3, cache.stats().missCount());
230   }
231 
232   // asMap tests
233 
testAsMap()234   public void testAsMap() {
235     CacheBuilder<Object, Object> builder = createCacheBuilder();
236     LocalLoadingCache<Object, Object> cache = makeCache(builder, identityLoader());
237     assertEquals(EMPTY_STATS, cache.stats());
238 
239     Object one = new Object();
240     Object two = new Object();
241     Object three = new Object();
242 
243     ConcurrentMap<Object, Object> map = cache.asMap();
244     assertNull(map.put(one, two));
245     assertSame(two, map.get(one));
246     map.putAll(ImmutableMap.of(two, three));
247     assertSame(three, map.get(two));
248     assertSame(two, map.putIfAbsent(one, three));
249     assertSame(two, map.get(one));
250     assertNull(map.putIfAbsent(three, one));
251     assertSame(one, map.get(three));
252     assertSame(two, map.replace(one, three));
253     assertSame(three, map.get(one));
254     assertFalse(map.replace(one, two, three));
255     assertSame(three, map.get(one));
256     assertTrue(map.replace(one, three, two));
257     assertSame(two, map.get(one));
258     assertEquals(3, map.size());
259 
260     map.clear();
261     assertTrue(map.isEmpty());
262     assertEquals(0, map.size());
263 
264     cache.getUnchecked(one);
265     assertEquals(1, map.size());
266     assertSame(one, map.get(one));
267     assertTrue(map.containsKey(one));
268     assertTrue(map.containsValue(one));
269     assertSame(one, map.remove(one));
270     assertEquals(0, map.size());
271 
272     cache.getUnchecked(one);
273     assertEquals(1, map.size());
274     assertFalse(map.remove(one, two));
275     assertTrue(map.remove(one, one));
276     assertEquals(0, map.size());
277 
278     cache.getUnchecked(one);
279     Map<Object, Object> newMap = ImmutableMap.of(one, one);
280     assertEquals(newMap, map);
281     assertEquals(newMap.entrySet(), map.entrySet());
282     assertEquals(newMap.keySet(), map.keySet());
283     Set<Object> expectedValues = ImmutableSet.of(one);
284     Set<Object> actualValues = ImmutableSet.copyOf(map.values());
285     assertEquals(expectedValues, actualValues);
286   }
287 
288   /**
289    * Lookups on the map view shouldn't impact the recency queue.
290    */
testAsMapRecency()291   public void testAsMapRecency() {
292     CacheBuilder<Object, Object> builder = createCacheBuilder()
293         .concurrencyLevel(1)
294         .maximumSize(SMALL_MAX_SIZE);
295     LocalLoadingCache<Object, Object> cache = makeCache(builder, identityLoader());
296     Segment<Object, Object> segment = cache.localCache.segments[0];
297     ConcurrentMap<Object, Object> map = cache.asMap();
298 
299     Object one = new Object();
300     assertSame(one, cache.getUnchecked(one));
301     assertTrue(segment.recencyQueue.isEmpty());
302     assertSame(one, map.get(one));
303     assertSame(one, segment.recencyQueue.peek().getKey());
304     assertSame(one, cache.getUnchecked(one));
305     assertFalse(segment.recencyQueue.isEmpty());
306   }
307 
testRecursiveComputation()308   public void testRecursiveComputation() throws InterruptedException {
309     final AtomicReference<LoadingCache<Integer, String>> cacheRef =
310         new AtomicReference<LoadingCache<Integer, String>>();
311     CacheLoader<Integer, String> recursiveLoader = new CacheLoader<Integer, String>() {
312       @Override
313       public String load(Integer key) {
314         if (key > 0) {
315           return key + ", " + cacheRef.get().getUnchecked(key - 1);
316         } else {
317           return "0";
318         }
319       }
320     };
321 
322     LoadingCache<Integer, String> recursiveCache = new CacheBuilder<Integer, String>()
323         .weakKeys()
324         .weakValues()
325         .build(recursiveLoader);
326     cacheRef.set(recursiveCache);
327     assertEquals("3, 2, 1, 0", recursiveCache.getUnchecked(3));
328 
329     recursiveLoader = new CacheLoader<Integer, String>() {
330       @Override
331       public String load(Integer key) {
332         return cacheRef.get().getUnchecked(key);
333       }
334     };
335 
336     recursiveCache = new CacheBuilder<Integer, String>()
337         .weakKeys()
338         .weakValues()
339         .build(recursiveLoader);
340     cacheRef.set(recursiveCache);
341 
342     // tells the test when the compution has completed
343     final CountDownLatch doneSignal = new CountDownLatch(1);
344 
345     Thread thread = new Thread() {
346       @Override
347       public void run() {
348         try {
349           cacheRef.get().getUnchecked(3);
350         } finally {
351           doneSignal.countDown();
352         }
353       }
354     };
355     thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
356       @Override
357       public void uncaughtException(Thread t, Throwable e) {}
358     });
359     thread.start();
360 
361     boolean done = doneSignal.await(1, TimeUnit.SECONDS);
362     if (!done) {
363       StringBuilder builder = new StringBuilder();
364       for (StackTraceElement trace : thread.getStackTrace()) {
365         builder.append("\tat ").append(trace).append('\n');
366       }
367       fail(builder.toString());
368     }
369   }
370 }
371