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.RequestContext;
13 import com.atlassian.vcache.internal.core.DefaultRequestContext;
14 import com.atlassian.vcache.internal.core.PlainExternalCacheKeyGenerator;
15 import net.spy.memcached.MemcachedClientIF;
16 import net.spy.memcached.OperationTimeoutException;
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.Map;
25 import java.util.Optional;
26 import java.util.concurrent.CompletionStage;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.Future;
29
30 import static com.atlassian.vcache.ExternalCacheException.Reason.UNCLASSIFIED_FAILURE;
31 import static com.atlassian.vcache.VCacheUtils.fold;
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 org.hamcrest.MatcherAssert.assertThat;
36 import static org.hamcrest.Matchers.is;
37 import static org.hamcrest.Matchers.not;
38 import static org.mockito.Matchers.eq;
39 import static org.mockito.Mockito.times;
40 import static org.mockito.Mockito.verify;
41 import static org.mockito.Mockito.verifyNoMoreInteractions;
42 import static org.mockito.Mockito.when;
43
44 @RunWith(MockitoJUnitRunner.class)
45 public class MemcachedDirectExternalCacheTest {
46 private static final int DEFAULT_TTL = 6;
47
48 @Mock
49 private MemcachedClientIF memClient;
50
51 @Mock
52 private Future<Boolean> booleanFuture;
53
54 private RequestContext requestContext = new DefaultRequestContext("tenant-id");
55 private DirectExternalCache<String> cache;
56
57 @Before
58 public void init() {
59 final ExternalCacheSettings settings = new ExternalCacheSettingsBuilder()
60 .entryGrowthRateHint(ChangeRate.LOW_CHANGE)
61 .entryCountHint(5)
62 .defaultTtl(Duration.ofSeconds(DEFAULT_TTL))
63 .dataChangeRateHint(ChangeRate.HIGH_CHANGE)
64 .build();
65 cache = new MemcachedDirectExternalCache<>(
66 () -> memClient,
67 () -> requestContext,
68 new PlainExternalCacheKeyGenerator("prodid"),
69 "mocked", StringMarshalling.pair(), settings);
70 }
71
72 @Test
73 public void get_missing() throws Exception {
74 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
75 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing"))).thenReturn(null);
76
77 final CompletionStage<Optional<String>> f1 = cache.get("missing");
78
79 assertThat(f1, successfulWith(is(Optional.empty())));
80
81 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
82 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
83 verifyNoMoreInteractions(memClient);
84 }
85
86 @Test
87 public void get_exists() throws Exception {
88 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
89 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
90 .thenReturn(new StringMarshalling().marshallToBytes("little creatures"));
91
92 final CompletionStage<Optional<String>> f1 = cache.get("missing");
93
94 assertThat(f1, successfulWith(is(Optional.of("little creatures"))));
95
96 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
97 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
98 verifyNoMoreInteractions(memClient);
99 }
100
101 @Test
102 public void get_supplier_missing_okay_add() throws Exception {
103
104 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
105 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing"))).thenReturn(null);
106 when(booleanFuture.get()).thenReturn(true);
107 when(memClient.add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied"))))
108 .thenReturn(booleanFuture);
109
110 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
111
112 assertThat(f1, successfulWith(is("supplied")));
113
114 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
115 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
116 verify(memClient).add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied")));
117 verifyNoMoreInteractions(memClient);
118 }
119
120 @Test
121 public void get_supplier_missing_fail_add_as_other_thread_added() throws Exception {
122
123 when(booleanFuture.get()).thenReturn(false);
124 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
125 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
126 .thenReturn(
127 null,
128 new StringMarshalling().marshallToBytes("added by another thread"));
129 when(memClient.add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied"))))
130 .thenReturn(booleanFuture);
131
132 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
133
134 assertThat(f1, successfulWith(is("added by another thread")));
135
136 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
137 verify(memClient, times(2)).get(eq("prodid::tenant-id::mocked::3::missing"));
138 verify(memClient).add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied")));
139 verifyNoMoreInteractions(memClient);
140 }
141
142 @Test
143 public void get_supplier_exists() throws Exception {
144 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
145 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
146 .thenReturn(new StringMarshalling().marshallToBytes("little creatures"));
147
148 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
149
150 assertThat(f1, successfulWith(is("little creatures")));
151
152 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
153 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
154 verifyNoMoreInteractions(memClient);
155 }
156
157 @Test
158 public void get_timeout() {
159 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
160 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
161 .thenThrow(new OperationTimeoutException("forced"));
162
163 final CompletionStage<Optional<String>> f1 = cache.get("missing");
164
165 assertThat(f1, not(successful()));
166
167 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
168 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
169 verifyNoMoreInteractions(memClient);
170 }
171
172 @Test
173 public void put_policy_set() throws Exception {
174 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
175 when(booleanFuture.get()).thenReturn(true);
176 when(memClient.set(
177 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
178 eq(new StringMarshalling().marshallToBytes("value"))))
179 .thenReturn(booleanFuture);
180
181 final CompletionStage<Boolean> f2 = cache.put("missing", "value", PutPolicy.PUT_ALWAYS);
182
183 assertThat(f2, successfulWith(is(true)));
184
185 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
186 verify(memClient).set(
187 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
188 eq(new StringMarshalling().marshallToBytes("value")));
189 verifyNoMoreInteractions(memClient);
190 }
191
192 @Test
193 public void safeExtractId_wrong_type() {
194 final CompletionStage<Boolean> f1 = cache.replaceIf("bad-cas", new CasIdentifier() {
195 }, "ignored");
196
197 assertThat(f1, not(successful()));
198
199 final boolean okay = fold(
200 f1,
201 success -> false,
202 throwable -> (throwable instanceof ExternalCacheException)
203 && (((ExternalCacheException) throwable).getReason() == UNCLASSIFIED_FAILURE));
204 assertThat(okay, is(true));
205 }
206
207 @Test
208 public void getBulk_no_keys() throws ExecutionException, InterruptedException {
209 final CompletionStage<Map<String, Optional<String>>> get = cache.getBulk();
210
211 assertThat(get, successful());
212 assertThat(unsafeJoin(get).isEmpty(), is(true));
213 verifyNoMoreInteractions(memClient);
214 }
215
216 @Test
217 public void getBulk_function_no_keys() throws ExecutionException, InterruptedException {
218 final CompletionStage<Map<String, String>> get = cache.getBulk(keys -> null);
219
220 assertThat(get, successful());
221 assertThat(unsafeJoin(get).isEmpty(), is(true));
222 verifyNoMoreInteractions(memClient);
223 }
224
225 @Test
226 public void getBulkIdentified_no_keys() throws ExecutionException, InterruptedException {
227 final CompletionStage<Map<String, Optional<IdentifiedValue<String>>>> get = cache.getBulkIdentified();
228
229 assertThat(get, successful());
230 assertThat(unsafeJoin(get).isEmpty(), is(true));
231 verifyNoMoreInteractions(memClient);
232 }
233
234 @Test
235 public void remove_no_keys() throws ExecutionException, InterruptedException {
236 final CompletionStage<Void> get = cache.remove();
237
238 assertThat(get, successful());
239 verifyNoMoreInteractions(memClient);
240 }
241 }