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",
70 StringMarshalling.pair(),
71 settings,
72 Duration.ofSeconds(1));
73 }
74
75 @Test
76 public void get_missing() throws Exception {
77 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
78 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing"))).thenReturn(null);
79
80 final CompletionStage<Optional<String>> f1 = cache.get("missing");
81
82 assertThat(f1, successfulWith(is(Optional.empty())));
83
84 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
85 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
86 verifyNoMoreInteractions(memClient);
87 }
88
89 @Test
90 public void get_exists() throws Exception {
91 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
92 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
93 .thenReturn(new StringMarshalling().marshallToBytes("little creatures"));
94
95 final CompletionStage<Optional<String>> f1 = cache.get("missing");
96
97 assertThat(f1, successfulWith(is(Optional.of("little creatures"))));
98
99 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
100 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
101 verifyNoMoreInteractions(memClient);
102 }
103
104 @Test
105 public void get_supplier_missing_okay_add() throws Exception {
106
107 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
108 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing"))).thenReturn(null);
109 when(booleanFuture.get()).thenReturn(true);
110 when(memClient.add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied"))))
111 .thenReturn(booleanFuture);
112
113 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
114
115 assertThat(f1, successfulWith(is("supplied")));
116
117 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
118 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
119 verify(memClient).add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied")));
120 verifyNoMoreInteractions(memClient);
121 }
122
123 @Test
124 public void get_supplier_missing_fail_add_as_other_thread_added() throws Exception {
125
126 when(booleanFuture.get()).thenReturn(false);
127 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
128 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
129 .thenReturn(
130 null,
131 new StringMarshalling().marshallToBytes("added by another thread"));
132 when(memClient.add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied"))))
133 .thenReturn(booleanFuture);
134
135 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
136
137 assertThat(f1, successfulWith(is("added by another thread")));
138
139 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
140 verify(memClient, times(2)).get(eq("prodid::tenant-id::mocked::3::missing"));
141 verify(memClient).add(eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL), eq(new StringMarshalling().marshallToBytes("supplied")));
142 verifyNoMoreInteractions(memClient);
143 }
144
145 @Test
146 public void get_supplier_exists() throws Exception {
147 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
148 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
149 .thenReturn(new StringMarshalling().marshallToBytes("little creatures"));
150
151 final CompletionStage<String> f1 = cache.get("missing", () -> "supplied");
152
153 assertThat(f1, successfulWith(is("little creatures")));
154
155 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
156 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
157 verifyNoMoreInteractions(memClient);
158 }
159
160 @Test
161 public void get_timeout() {
162 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
163 when(memClient.get(eq("prodid::tenant-id::mocked::3::missing")))
164 .thenThrow(new OperationTimeoutException("forced"));
165
166 final CompletionStage<Optional<String>> f1 = cache.get("missing");
167
168 assertThat(f1, not(successful()));
169
170 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
171 verify(memClient).get(eq("prodid::tenant-id::mocked::3::missing"));
172 verifyNoMoreInteractions(memClient);
173 }
174
175 @Test
176 public void put_policy_set() throws Exception {
177 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
178 when(booleanFuture.get()).thenReturn(true);
179 when(memClient.set(
180 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
181 eq(new StringMarshalling().marshallToBytes("value"))))
182 .thenReturn(booleanFuture);
183
184 final CompletionStage<Boolean> f2 = cache.put("missing", "value", PutPolicy.PUT_ALWAYS);
185
186 assertThat(f2, successfulWith(is(true)));
187
188 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
189 verify(memClient).set(
190 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
191 eq(new StringMarshalling().marshallToBytes("value")));
192 verifyNoMoreInteractions(memClient);
193 }
194
195 @Test
196 public void safeExtractId_wrong_type() {
197 final CompletionStage<Boolean> f1 = cache.replaceIf("bad-cas", new CasIdentifier() {
198 }, "ignored");
199
200 assertThat(f1, not(successful()));
201
202 final boolean okay = fold(
203 f1,
204 success -> false,
205 throwable -> (throwable instanceof ExternalCacheException)
206 && (((ExternalCacheException) throwable).getReason() == UNCLASSIFIED_FAILURE));
207 assertThat(okay, is(true));
208 }
209
210 @Test
211 public void getBulk_no_keys() throws ExecutionException, InterruptedException {
212 final CompletionStage<Map<String, Optional<String>>> get = cache.getBulk();
213
214 assertThat(get, successful());
215 assertThat(unsafeJoin(get).isEmpty(), is(true));
216 verifyNoMoreInteractions(memClient);
217 }
218
219 @Test
220 public void getBulk_function_no_keys() throws ExecutionException, InterruptedException {
221 final CompletionStage<Map<String, String>> get = cache.getBulk(keys -> null);
222
223 assertThat(get, successful());
224 assertThat(unsafeJoin(get).isEmpty(), is(true));
225 verifyNoMoreInteractions(memClient);
226 }
227
228 @Test
229 public void getBulkIdentified_no_keys() throws ExecutionException, InterruptedException {
230 final CompletionStage<Map<String, Optional<IdentifiedValue<String>>>> get = cache.getBulkIdentified();
231
232 assertThat(get, successful());
233 assertThat(unsafeJoin(get).isEmpty(), is(true));
234 verifyNoMoreInteractions(memClient);
235 }
236
237 @Test
238 public void remove_no_keys() throws ExecutionException, InterruptedException {
239 final CompletionStage<Void> get = cache.remove();
240
241 assertThat(get, successful());
242 verifyNoMoreInteractions(memClient);
243 }
244 }