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