View Javadoc

1   package com.atlassian.vcache.internal.memcached;
2   
3   import com.atlassian.marshalling.jdk.StringMarshalling;
4   import com.atlassian.vcache.ChangeRate;
5   import com.atlassian.vcache.ExternalCacheSettings;
6   import com.atlassian.vcache.ExternalCacheSettingsBuilder;
7   import com.atlassian.vcache.PutPolicy;
8   import com.atlassian.vcache.StableReadExternalCache;
9   import com.atlassian.vcache.internal.MetricLabel;
10  import com.atlassian.vcache.internal.RequestContext;
11  import com.atlassian.vcache.internal.core.DefaultRequestContext;
12  import com.atlassian.vcache.internal.core.PlainExternalCacheKeyGenerator;
13  import com.atlassian.vcache.internal.core.metrics.CacheType;
14  import com.atlassian.vcache.internal.core.metrics.MetricsRecorder;
15  import com.google.common.collect.ImmutableMap;
16  import net.spy.memcached.MemcachedClientIF;
17  import org.junit.Before;
18  import org.junit.Test;
19  import org.junit.runner.RunWith;
20  import org.mockito.Mock;
21  import org.mockito.runners.MockitoJUnitRunner;
22  
23  import java.time.Duration;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.concurrent.CompletionStage;
29  import java.util.concurrent.ExecutionException;
30  import java.util.concurrent.Future;
31  
32  import static com.atlassian.vcache.VCacheUtils.unsafeJoin;
33  import static com.atlassian.vcache.internal.test.CompletionStageSuccessful.successful;
34  import static com.atlassian.vcache.internal.test.CompletionStageSuccessful.successfulWith;
35  import static com.google.common.collect.Maps.asMap;
36  import static com.google.common.collect.Sets.newHashSet;
37  import static org.hamcrest.MatcherAssert.assertThat;
38  import static org.hamcrest.Matchers.containsInAnyOrder;
39  import static org.hamcrest.Matchers.is;
40  import static org.mockito.Matchers.eq;
41  import static org.mockito.Mockito.times;
42  import static org.mockito.Mockito.verify;
43  import static org.mockito.Mockito.verifyNoMoreInteractions;
44  import static org.mockito.Mockito.verifyZeroInteractions;
45  import static org.mockito.Mockito.when;
46  
47  @SuppressWarnings("NullableProblems")
48  @RunWith(MockitoJUnitRunner.class)
49  public class MemcachedStableReadExternalCacheTest {
50  
51      private static final int DEFAULT_TTL = 6;
52  
53      @Mock
54      private MemcachedClientIF memClient;
55  
56      @Mock
57      private Future<Boolean> booleanFuture;
58  
59      @Mock
60      private MetricsRecorder metricsRecorder;
61  
62      private final RequestContext requestContext = new DefaultRequestContext(() -> "tenant-id");
63      private StableReadExternalCache<String> cache;
64  
65      @Before
66      public void init() {
67          final ExternalCacheSettings settings = new ExternalCacheSettingsBuilder()
68                  .entryGrowthRateHint(ChangeRate.LOW_CHANGE)
69                  .entryCountHint(5)
70                  .defaultTtl(Duration.ofSeconds(DEFAULT_TTL))
71                  .dataChangeRateHint(ChangeRate.HIGH_CHANGE)
72                  .build();
73          cache = new MemcachedStableReadExternalCache<>(
74                  Utils.defaultServiceSettingsBuilder(() -> memClient, Duration.ofSeconds(5)).build(),
75                  () -> requestContext,
76                  new PlainExternalCacheKeyGenerator("prodid"),
77                  "mocked",
78                  StringMarshalling.pair(),
79                  settings,
80                  metricsRecorder);
81      }
82  
83      @Test
84      public void put_policy_set() throws Exception {
85          when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
86          when(booleanFuture.get()).thenReturn(true);
87          when(memClient.set(
88                  eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
89                  eq(new StringMarshalling().marshallToBytes("value"))))
90                  .thenReturn(booleanFuture);
91  
92          final CompletionStage<Boolean> f2 = cache.put("missing", "value", PutPolicy.PUT_ALWAYS);
93  
94          assertThat(f2, successfulWith(is(true)));
95  
96          verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
97          verify(memClient).set(
98                  eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
99                  eq(new StringMarshalling().marshallToBytes("value")));
100         verifyNoMoreInteractions(memClient);
101     }
102 
103     @Test
104     public void get_with_supplier_works_memcached_exception() {
105         byte[] supplier_values = new StringMarshalling().marshallToBytes("supplier_value");
106         when(memClient.add(
107                 eq("prodid::tenant-id::mocked::0::broken"),
108                 eq(DEFAULT_TTL),
109                 eq(supplier_values)))
110                 .thenThrow(new IllegalStateException());
111 
112         final CompletionStage<String> f2 = cache.get("broken", () -> "supplier_value");
113         assertThat(f2.toCompletableFuture().join(), is("supplier_value"));
114         verify(memClient).add(eq("prodid::tenant-id::mocked::0::broken"), eq(DEFAULT_TTL), eq(supplier_values));
115         verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
116 
117         // Go back and see if we can load the value again from the local cache.
118         CompletionStage<Optional<String>> f3 = cache.get("broken");
119         assertThat(f3.toCompletableFuture().join().isPresent(), is(true));
120         assertThat(f3.toCompletableFuture().join().get(), is("supplier_value"));
121         verify(memClient).get(eq("prodid::tenant-id::mocked::0::broken"));
122         verifyNoMoreInteractions(memClient);
123     }
124 
125     @Test
126     public void getBulk_with_supplier_works_memcached_exception() {
127         when(memClient.add(
128                 eq("prodid::tenant-id::mocked::0::bulkbroken"),
129                 eq(DEFAULT_TTL),
130                 eq(new StringMarshalling().marshallToBytes("supplier_value"))))
131                 .thenThrow(new IllegalStateException());
132 
133         final CompletionStage<Map<String, String>> f2 = cache.getBulk(keys -> ImmutableMap.of("bulkbroken", "supplier_value"), "bulkbroken");
134         assertThat(f2.toCompletableFuture().join().get("bulkbroken"), is("supplier_value"));
135 
136 
137     }
138 
139     @Test
140     public void getBulk_no_keys() throws ExecutionException, InterruptedException {
141         final CompletionStage<Map<String, Optional<String>>> get = cache.getBulk();
142 
143         assertThat(get, successful());
144         assertThat(unsafeJoin(get).isEmpty(), is(true));
145         verifyNoMoreInteractions(memClient);
146     }
147 
148     @Test
149     public void getBulk_function_no_keys() throws ExecutionException, InterruptedException {
150         final CompletionStage<Map<String, String>> get = cache.getBulk(keys -> null);
151 
152         assertThat(get, successful());
153         assertThat(unsafeJoin(get).isEmpty(), is(true));
154         verifyNoMoreInteractions(memClient);
155     }
156 
157     @Test
158     public void remove_no_keys() throws ExecutionException, InterruptedException {
159         final CompletionStage<Void> rm1 = cache.remove();
160 
161         assertThat(rm1, successful());
162         verifyZeroInteractions(memClient);
163     }
164 
165     @Test
166     public void removeAll_getBulk() throws Exception {
167         when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
168         when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L))).thenReturn(4L);
169 
170         final CompletionStage<Void> rm1 = cache.removeAll();
171 
172         assertThat(rm1, successful());
173         verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L));
174         verifyNoMoreInteractions(memClient);
175 
176         final HashSet<String> externalKeys = newHashSet(
177                 "prodid::tenant-id::mocked::4::key-1", "prodid::tenant-id::mocked::4::key-2");
178         final Map<String, Object> externalResult = new HashMap<>();
179         externalResult.put("prodid::tenant-id::mocked::4::key-1", null);
180         externalResult.put("prodid::tenant-id::mocked::4::key-2", null);
181         when(memClient.getBulk(eq(externalKeys))).thenReturn(externalResult);
182 
183         final CompletionStage<Map<String, Optional<String>>> get1 = cache.getBulk("key-1", "key-2");
184 
185         assertThat(get1, successful());
186         assertThat(unsafeJoin(get1).keySet(), containsInAnyOrder("key-1", "key-2"));
187         //noinspection unchecked
188         assertThat(unsafeJoin(get1).values(), containsInAnyOrder(Optional.empty(), Optional.empty()));
189 
190         verify(memClient).getBulk(eq(externalKeys));
191         verifyNoMoreInteractions(memClient);
192     }
193 
194     @Test
195     public void removeAll_getBulkFactory() throws Exception {
196         when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
197         when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L))).thenReturn(4L);
198 
199         final CompletionStage<Void> rm1 = cache.removeAll();
200 
201         assertThat(rm1, successful());
202         verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L));
203         verifyNoMoreInteractions(memClient);
204 
205         final HashSet<String> externalKeys = newHashSet(
206                 "prodid::tenant-id::mocked::4::key-1", "prodid::tenant-id::mocked::4::key-2");
207         final Map<String, Object> externalResult = new HashMap<>();
208         externalResult.put(
209                 "prodid::tenant-id::mocked::4::key-1",
210                 new StringMarshalling().marshallToBytes("ice-monkeys"));
211         when(memClient.getBulk(eq(externalKeys))).thenReturn(externalResult);
212         when(memClient.add(
213                 eq("prodid::tenant-id::mocked::4::key-2"),
214                 eq(DEFAULT_TTL),
215                 eq(new StringMarshalling().marshallToBytes("key-2-1")))
216         ).thenReturn(booleanFuture);
217         when(booleanFuture.get()).thenReturn(true);
218 
219         final CompletionStage<Map<String, String>> get1 = cache.getBulk(
220                 keys -> asMap(keys, k -> k + "-1"), "key-1", "key-2");
221 
222         assertThat(get1, successful());
223         assertThat(unsafeJoin(get1).keySet(), containsInAnyOrder("key-1", "key-2"));
224         assertThat(unsafeJoin(get1).values(), containsInAnyOrder("ice-monkeys", "key-2-1"));
225 
226         verify(memClient).getBulk(eq(externalKeys));
227         verify(memClient).add(
228                 eq("prodid::tenant-id::mocked::4::key-2"),
229                 eq(DEFAULT_TTL),
230                 eq(new StringMarshalling().marshallToBytes("key-2-1")));
231         verifyNoMoreInteractions(memClient);
232     }
233 
234     @Test
235     public void getBulk_metrics() throws Exception {
236         when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(4L);
237 
238         final HashSet<String> externalKeys = newHashSet(
239                 "prodid::tenant-id::mocked::4::key-1", "prodid::tenant-id::mocked::4::key-2");
240         final Map<String, Object> externalResult = new HashMap<>();
241         externalResult.put(
242                 "prodid::tenant-id::mocked::4::key-1",
243                 new StringMarshalling().marshallToBytes("ice-monkeys"));
244         when(memClient.getBulk(eq(externalKeys))).thenReturn(externalResult);
245         when(memClient.add(
246                 eq("prodid::tenant-id::mocked::4::key-2"),
247                 eq(DEFAULT_TTL),
248                 eq(new StringMarshalling().marshallToBytes("key-2-1")))
249         ).thenReturn(booleanFuture);
250         when(booleanFuture.get()).thenReturn(true);
251 
252         final CompletionStage<Map<String, String>> get1 = cache.getBulk(
253                 keys -> asMap(keys, k -> k + "-1"), "key-1", "key-2");
254 
255         assertThat(get1, successful());
256         assertThat(unsafeJoin(get1).keySet(), containsInAnyOrder("key-1", "key-2"));
257         assertThat(unsafeJoin(get1).values(), containsInAnyOrder("ice-monkeys", "key-2-1"));
258 
259         verify(metricsRecorder, times(2)).record(eq("mocked"), eq(CacheType.EXTERNAL), eq(MetricLabel.NUMBER_OF_REMOTE_GET), eq(1L));
260         verifyNoMoreInteractions(metricsRecorder);
261     }
262 
263     @Test
264     public void get_metrics() throws Exception {
265         when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(4L);
266         when(memClient.get(eq("prodid::tenant-id::mocked::4::key-1")))
267                 .thenReturn(new StringMarshalling().marshallToBytes("it exists"));
268 
269         final CompletionStage<String> get1 = cache.get("key-1", () -> "value-1");
270 
271         assertThat(get1, successful());
272 
273         verify(metricsRecorder).record(eq("mocked"), eq(CacheType.EXTERNAL), eq(MetricLabel.NUMBER_OF_REMOTE_GET), eq(1L));
274         verifyNoMoreInteractions(metricsRecorder);
275     }
276 }