View Javadoc

1   package com.atlassian.cache;
2   
3   import java.util.concurrent.Callable;
4   import java.util.concurrent.CountDownLatch;
5   import java.util.concurrent.ExecutionException;
6   import java.util.concurrent.ExecutorService;
7   import java.util.concurrent.Executors;
8   import java.util.concurrent.Future;
9   import java.util.concurrent.TimeUnit;
10  import java.util.concurrent.TimeoutException;
11  import java.util.concurrent.atomic.AtomicBoolean;
12  import java.util.concurrent.atomic.AtomicInteger;
13  import java.util.concurrent.atomic.AtomicLong;
14  import javax.annotation.Nonnull;
15  
16  import org.hamcrest.Matchers;
17  import org.junit.Test;
18  
19  import static org.hamcrest.Matchers.hasSize;
20  import static org.hamcrest.Matchers.is;
21  import static org.junit.Assert.assertThat;
22  
23  public abstract class AbstractCacheTest
24  {
25      @Test
26      public void testRemoveFromCacheWhileLoadingCacheValueReturnsLatestValue()
27              throws InterruptedException, TimeoutException, ExecutionException
28      {
29          testWhenCacheValueManipulatedNewValueReturned(new CacheManipulator<String, Long>()
30          {
31              @Override
32              public void manipulateKeyOn(final String key, final Cache<String, Long> cache)
33              {
34                  cache.remove(key);
35              }
36          });
37      }
38  
39      /**
40       * Confirms that when a remove of a key with a specific value is called, if there is a loading call which is paused
41       * we wait for that load to finish before starting the remove.
42       *
43       * <b>Note</b> this test will become flakey when the code covering the code path is bad.
44       * @throws InterruptedException
45       * @throws TimeoutException
46       * @throws ExecutionException
47       */
48      @Test
49      public void testRemoveSpecificValueFromCacheWhileLoadingCacheValueReturnsLatestValue()
50              throws InterruptedException, TimeoutException, ExecutionException
51      {
52          testWhenCacheValueManipulatedNewValueReturned(new CacheManipulator<String, Long>()
53          {
54              @Override
55              public void manipulateKeyOn(final String key, final Cache<String, Long> cache)
56              {
57                  //the initial value of the cache should be 1. In essence this test should operate in the same way that
58                  //#testRemoveFromCacheWhileLoadingCacheValueReturnsLatestValue does
59                  cache.remove(key, 1L);
60              }
61          });
62      }
63  
64      protected CacheFactory factory;
65  
66      protected Cache<String,Long> makeExceptionalCache()
67      {
68          // Build a Cache using the builder
69          CacheLoader<String, Long> loader = new CacheLoader<String, Long>()
70          {
71              @Nonnull
72              @Override
73              public Long load(@Nonnull final String key)
74              {
75                  return Long.valueOf(key);
76              }
77          };
78          final Cache<String, Long> cache = factory.getCache("mycache", loader, settingsBuilder().build());
79          assertEmpty(cache);
80          return cache;
81      }
82  
83      protected Cache<String, Long> makeExpiringCache()
84      {
85          // Build a Cache using the builder
86          CacheLoader<String, Long> loader = new CacheLoader<String, Long>()
87          {
88              @Nonnull
89              @Override
90              public Long load(@Nonnull final String key)
91              {
92                  try
93                  {
94                      return Long.valueOf(key);
95                  }
96                  catch (NumberFormatException e)
97                  {
98                      return -21L;
99                  }
100             }
101         };
102         CacheSettings settings = settingsBuilder().expireAfterAccess(10, TimeUnit.MILLISECONDS).build();
103         final Cache<String, Long> cache = factory.getCache("mycache", loader, settings);
104         assertEmpty(cache);
105         return cache;
106     }
107 
108     protected Cache<String,Long> makeNullReturningCache()
109     {
110         // Build a Cache using the builder
111         CacheLoader<String, Long> loader = new CacheLoader<String, Long>()
112         {
113             @Nonnull
114             @Override
115             public Long load(@Nonnull final String key)
116             {
117                 try
118                 {
119                     return Long.valueOf(key);
120                 }
121                 catch (NumberFormatException e)
122                 {
123                     return null;
124                 }
125             }
126         };
127         final Cache<String, Long> cache = factory.getCache("mycache", loader, settingsBuilder().build());
128         assertEmpty(cache);
129         return cache;
130     }
131 
132     protected Cache<String, Long> makeSimpleCache()
133     {
134         // Build a Cache using the builder
135         final Cache<String, Long> cache = factory.getCache("mycache", null, settingsBuilder().build());
136         assertEmpty(cache);
137         return cache;
138     }
139 
140     protected Cache<String, Long> makeSizeLimitedCache(int maxEntries)
141     {
142         CacheSettings required = settingsBuilder().maxEntries(maxEntries).build();
143         final Cache<String, Long> cache = factory.getCache("mycache", null, required);
144         assertEmpty(cache);
145         return cache;
146     }
147 
148     protected Cache<String, Long> makeSizeLimitedCache(final int maxEntries, final AtomicInteger loadCounter)
149     {
150         // Build a Cache using the builder
151         CacheLoader<String, Long> loader = new CacheLoader<String, Long>()
152         {
153             @Nonnull
154             @Override
155             public Long load(@Nonnull final String key)
156             {
157                 loadCounter.incrementAndGet();
158                 return Long.valueOf(key);
159             }
160         };
161         CacheSettings settings = settingsBuilder().maxEntries(maxEntries).build();
162         final Cache<String, Long> cache = factory.getCache("mycache", loader, settings);
163         assertEmpty(cache);
164         return cache;
165     }
166 
167     protected Cache<String, Long> makeUnexpiringCache()
168     {
169         // Build a Cache using the builder
170         CacheLoader<String, Long> loader = new CacheLoader<String, Long>()
171         {
172             @Nonnull
173             @Override
174             public Long load(@Nonnull final String key)
175             {
176                 try
177                 {
178                     return Long.valueOf(key);
179                 }
180                 catch (NumberFormatException e)
181                 {
182                     return -21L;
183                 }
184             }
185         };
186         final Cache<String, Long> cache = factory.getCache("mycache", loader, settingsBuilder().build());
187         assertEmpty(cache);
188         return cache;
189     }
190 
191     protected CacheSettingsBuilder settingsBuilder()
192     {
193         return new CacheSettingsBuilder();
194     }
195 
196     // Hiding broken type inferences
197 
198     protected static <K,V> void assertEmpty(Cache<K,V> cache)
199     {
200         assertThat(cache.getKeys(), Matchers.<K>empty());
201     }
202 
203     protected static <K,V> void assertSize(Cache<K,V> cache, final int expectedSize)
204     {
205         assertThat(cache.getKeys(), hasSize(expectedSize));
206     }
207 
208     protected Cache<String, Long> makeCacheUsingLoader(final CacheLoader<String, Long> loader)
209     {
210         final Cache<String, Long> cache = factory.getCache("mycache", loader, settingsBuilder().build());
211         return cache;
212     }
213 
214     private void testWhenCacheValueManipulatedNewValueReturned(final CacheManipulator<String, Long> remover)
215             throws InterruptedException, TimeoutException, ExecutionException
216     {
217         final CountDownLatch readyToRemove = new CountDownLatch(1);
218         final CountDownLatch hasStartedRemove = new CountDownLatch(1);
219         final AtomicLong cacheValue = new AtomicLong(1L);
220         final AtomicBoolean loaderHasReturned = new AtomicBoolean();
221         final String key = "keyvalue";
222         //Vis value can cause the tests to fail in CI depending on the speed of the agent running the tests
223         final CacheLoader<String, Long> loader = new CacheLoader<String, Long>()
224         {
225             @Nonnull
226             @Override
227             public Long load(@Nonnull final String key)
228             {
229                 try
230                 {
231                     //Get the initial value before signalling to the other thread that it's safe to increment the
232                     //initial value
233                     Long value = cacheValue.get();
234                     readyToRemove.countDown();
235                     //Each of the caches implements blocking in a different way
236                     // Guava holds a future waiting for the loader to return
237                     // Ehcache SelfPopulatingCache inherits from BlockingCache which locks on the key
238                     // Hazlecast locks on the key of the value for five minutes
239                     // The timeout prevents a deadlock when the call to the loader is blocked and the remove then also
240                     //waits on the blocked loader
241                     hasStartedRemove.await();
242                     //Give enough time for the outside thread to do its manipulation of the cache
243                     Thread.sleep(200l);
244                     loaderHasReturned.set(true);
245                     return value;
246                 }
247                 catch (InterruptedException e)
248                 {
249                     throw new RuntimeException(e);
250                 }
251             }
252         };
253 
254         final Cache<String, Long> pausingCache = makeCacheUsingLoader(loader);
255 
256         ExecutorService service = Executors.newFixedThreadPool(2);
257         Future<Long> f = service.submit(new Callable<Long>()
258         {
259             @Override
260             public Long call() throws Exception
261             {
262                 return pausingCache.get(key);
263             }
264         });
265 
266         readyToRemove.await();
267         Long newCacheValue = cacheValue.incrementAndGet();
268 
269         Future<Void> manipulationOp = service.submit(new Callable<Void>()
270         {
271             @Override
272             public Void call() throws Exception
273             {
274                 //Simulate a change to the underlying value
275                 hasStartedRemove.countDown();
276                 remover.manipulateKeyOn(key, pausingCache);
277                 int i = 0;
278                 while(!loaderHasReturned.get() && i < 20)
279                 {
280                     //Different hardware and jvms has us get to this point at different times we wait for at most a few seconds
281                     //for the loader to return then we consider whether we have failed or not
282                     Thread.sleep(100L);
283                 }
284                 return null;
285             }
286         });
287 
288         manipulationOp.get();
289 
290         assertThat(loaderHasReturned.get(), is(true));
291         Long initialCacheValue = f.get(400, TimeUnit.MILLISECONDS);
292 
293         Long cachedValue = pausingCache.get(key);
294         assertThat(initialCacheValue, is(1L));
295         assertThat(cachedValue, is(newCacheValue));
296     }
297 }