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
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
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 }