1 package com.atlassian.vcache.internal.memcached;
2
3 import com.atlassian.marshalling.jdk.StringMarshalling;
4 import com.atlassian.vcache.CasIdentifier;
5 import com.atlassian.vcache.ChangeRate;
6 import com.atlassian.vcache.DirectExternalCache;
7 import com.atlassian.vcache.ExternalCacheException;
8 import com.atlassian.vcache.ExternalCacheSettings;
9 import com.atlassian.vcache.ExternalCacheSettingsBuilder;
10 import com.atlassian.vcache.IdentifiedValue;
11 import com.atlassian.vcache.PutPolicy;
12 import com.atlassian.vcache.internal.ExternalCacheExceptionListener;
13 import com.atlassian.vcache.internal.RequestContext;
14 import com.atlassian.vcache.internal.core.DefaultRequestContext;
15 import com.atlassian.vcache.internal.core.PlainExternalCacheKeyGenerator;
16 import net.spy.memcached.MemcachedClientIF;
17 import net.spy.memcached.OperationTimeoutException;
18 import org.junit.Before;
19 import org.junit.Test;
20 import org.junit.runner.RunWith;
21 import org.mockito.ArgumentCaptor;
22 import org.mockito.Mock;
23 import org.mockito.runners.MockitoJUnitRunner;
24
25 import java.time.Duration;
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.ExternalCacheException.Reason.TIMEOUT;
33 import static com.atlassian.vcache.ExternalCacheException.Reason.UNCLASSIFIED_FAILURE;
34 import static com.atlassian.vcache.VCacheUtils.fold;
35 import static com.atlassian.vcache.VCacheUtils.unsafeJoin;
36 import static com.atlassian.vcache.internal.test.CompletionStageSuccessful.successful;
37 import static com.atlassian.vcache.internal.test.CompletionStageSuccessful.successfulWith;
38 import static org.hamcrest.MatcherAssert.assertThat;
39 import static org.hamcrest.Matchers.equalTo;
40 import static org.hamcrest.Matchers.is;
41 import static org.hamcrest.Matchers.not;
42 import static org.hamcrest.Matchers.notNullValue;
43 import static org.mockito.Matchers.eq;
44 import static org.mockito.Mockito.times;
45 import static org.mockito.Mockito.verify;
46 import static org.mockito.Mockito.verifyNoMoreInteractions;
47 import static org.mockito.Mockito.when;
48
49 @RunWith(MockitoJUnitRunner.class)
50 public class MemcachedDirectExternalCacheTest {
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 ExternalCacheExceptionListener exceptionListener;
61
62 private RequestContext requestContext = new DefaultRequestContext(() -> "tenant-id");
63 private DirectExternalCache<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 MemcachedDirectExternalCache<>(
74 Utils.defaultServiceSettingsBuilder(() -> memClient, Duration.ofSeconds(1))
75 .externalCacheExceptionListener(exceptionListener)
76 .build(),
77 () -> requestContext,
78 new PlainExternalCacheKeyGenerator("prodid"),
79 "mocked",
80 StringMarshalling.pair(),
81 settings);
82 }
83
84 @Test
85 public void get_missing() throws Exception {
86 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
87 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing"))).thenReturn(null);
88
89 final CompletionStage<Optional<String>> f1 = cache.get("missing");
90
91 assertThat(f1, successfulWith(is(Optional.empty())));
92
93 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
94 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
95 verifyNoMoreInteractions(memClient);
96 verifyNoMoreInteractions(exceptionListener);
97 }
98
99 @Test
100 public void get_exists() throws Exception {
101 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
102 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
103 .thenReturn(new StringMarshalling().marshallToBytes("little creatures"));
104
105 final CompletionStage<Optional<String>> f1 = cache.get("missing");
106
107 assertThat(f1, successfulWith(is(Optional.of("little creatures"))));
108
109 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
110 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
111 verifyNoMoreInteractions(memClient);
112 verifyNoMoreInteractions(exceptionListener);
113 }
114
115 @Test
116 public void get_supplier_missing_okay_add() throws Exception {
117
118 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
119 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing"))).thenReturn(null);
120 when(booleanFuture.get()).thenReturn(true);
121 when(memClient.add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied"))))
122 .thenReturn(booleanFuture);
123
124 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
125
126 assertThat(f1, successfulWith(is("supplied")));
127
128 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
129 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
130 verify(memClient).add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied")));
131 verifyNoMoreInteractions(memClient);
132 verifyNoMoreInteractions(exceptionListener);
133 }
134
135 @Test
136 public void get_supplier_missing_fail_add_as_other_thread_added() throws Exception {
137
138 when(booleanFuture.get()).thenReturn(false);
139 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
140 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
141 .thenReturn(
142 null,
143 new StringMarshalling().marshallToBytes("added by another thread"));
144 when(memClient.add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied"))))
145 .thenReturn(booleanFuture);
146
147 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
148
149 assertThat(f1, successfulWith(is("added by another thread")));
150
151 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
152 verify(memClient, times(2)).get(eq("prodid::tenant-id::mocked::3::missing"));
153 verify(memClient).add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied")));
154 verifyNoMoreInteractions(memClient);
155 verifyNoMoreInteractions(exceptionListener);
156 }
157
158 @Test
159 public void get_supplier_exists() throws Exception {
160 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
161 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
162 .thenReturn(new StringMarshalling().marshallToBytes("little creatures"));
163
164 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
165
166 assertThat(f1, successfulWith(is("little creatures")));
167
168 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
169 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
170 verifyNoMoreInteractions(memClient);
171 verifyNoMoreInteractions(exceptionListener);
172 }
173
174 @Test
175 public void get_timeout() {
176 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
177 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
178 .thenThrow(new OperationTimeoutException("forced"));
179
180 final CompletionStage<Optional<String>> f1 = cache.get("missing");
181
182 assertThat(f1, not(successful()));
183
184 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
185 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
186 verifyNoMoreInteractions(memClient);
187
188 final ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
189 final ArgumentCaptor<ExternalCacheException> exceptionCaptor = ArgumentCaptor.forClass(ExternalCacheException.class);
190 verify(exceptionListener).onThrow(nameCaptor.capture(), exceptionCaptor.capture());
191 assertThat(nameCaptor.getValue(), is("mocked"));
192 assertThat(exceptionCaptor.getValue(), notNullValue());
193 assertThat(exceptionCaptor.getValue().getReason(), equalTo(TIMEOUT));
194 verifyNoMoreInteractions(exceptionListener);
195 }
196
197 @Test
198 public void put_policy_set() throws Exception {
199 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
200 when(booleanFuture.get()).thenReturn(true);
201 when(memClient.set(
202 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
203 eq(new StringMarshalling().marshallToBytes("value"))))
204 .thenReturn(booleanFuture);
205
206 final CompletionStage<Boolean> f2 = cache.put("missing", "value", PutPolicy.PUT_ALWAYS);
207
208 assertThat(f2, successfulWith(is(true)));
209
210 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
211 verify(memClient).set(
212 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
213 eq(new StringMarshalling().marshallToBytes("value")));
214 verifyNoMoreInteractions(memClient);
215 verifyNoMoreInteractions(exceptionListener);
216 }
217
218 @Test
219 public void safeExtractId_wrong_type() {
220 final CompletionStage<Boolean> f1 = cache.replaceIf("bad-cas", new CasIdentifier() {
221 }, "ignored");
222
223 assertThat(f1, not(successful()));
224
225 final boolean okay = fold(
226 f1,
227 success -> false,
228 throwable -> (throwable instanceof ExternalCacheException)
229 && (((ExternalCacheException) throwable).getReason() == UNCLASSIFIED_FAILURE));
230 assertThat(okay, is(true));
231
232 final ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
233 final ArgumentCaptor<ExternalCacheException> exceptionCaptor = ArgumentCaptor.forClass(ExternalCacheException.class);
234 verify(exceptionListener).onThrow(nameCaptor.capture(), exceptionCaptor.capture());
235 assertThat(nameCaptor.getValue(), is("mocked"));
236 assertThat(exceptionCaptor.getValue(), notNullValue());
237 assertThat(exceptionCaptor.getValue().getReason(), equalTo(UNCLASSIFIED_FAILURE));
238 verifyNoMoreInteractions(exceptionListener);
239 }
240
241 @Test
242 public void getBulk_no_keys() throws ExecutionException, InterruptedException {
243 final CompletionStage<Map<String, Optional<String>>> get = cache.getBulk();
244
245 assertThat(get, successful());
246 assertThat(unsafeJoin(get).isEmpty(), is(true));
247 verifyNoMoreInteractions(memClient);
248 verifyNoMoreInteractions(exceptionListener);
249 }
250
251 @Test
252 public void getBulk_function_no_keys() throws ExecutionException, InterruptedException {
253 final CompletionStage<Map<String, String>> get = cache.getBulk(keys -> null);
254
255 assertThat(get, successful());
256 assertThat(unsafeJoin(get).isEmpty(), is(true));
257 verifyNoMoreInteractions(memClient);
258 verifyNoMoreInteractions(exceptionListener);
259 }
260
261 @Test
262 public void getBulkIdentified_no_keys() throws ExecutionException, InterruptedException {
263 final CompletionStage<Map<String, Optional<IdentifiedValue<String>>>> get = cache.getBulkIdentified();
264
265 assertThat(get, successful());
266 assertThat(unsafeJoin(get).isEmpty(), is(true));
267 verifyNoMoreInteractions(memClient);
268 verifyNoMoreInteractions(exceptionListener);
269 }
270
271 @Test
272 public void remove_no_keys() throws ExecutionException, InterruptedException {
273 final CompletionStage<Void> get = cache.remove();
274
275 assertThat(get, successful());
276 verifyNoMoreInteractions(memClient);
277 verifyNoMoreInteractions(exceptionListener);
278 }
279 }