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.Maps;
16 import com.google.common.collect.Sets;
17 import net.spy.memcached.MemcachedClientIF;
18 import org.junit.Before;
19 import org.junit.Test;
20 import org.junit.runner.RunWith;
21 import org.mockito.Mock;
22 import org.mockito.runners.MockitoJUnitRunner;
23
24 import java.time.Duration;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.concurrent.CompletionStage;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.Future;
32
33 import static com.atlassian.vcache.VCacheUtils.unsafeJoin;
34 import static com.atlassian.vcache.internal.test.CompletionStageSuccessful.successful;
35 import static com.atlassian.vcache.internal.test.CompletionStageSuccessful.successfulWith;
36 import static org.hamcrest.MatcherAssert.assertThat;
37 import static org.hamcrest.Matchers.containsInAnyOrder;
38 import static org.hamcrest.Matchers.is;
39 import static org.mockito.Matchers.eq;
40 import static org.mockito.Mockito.times;
41 import static org.mockito.Mockito.verify;
42 import static org.mockito.Mockito.verifyNoMoreInteractions;
43 import static org.mockito.Mockito.when;
44
45 @SuppressWarnings("NullableProblems")
46 @RunWith(MockitoJUnitRunner.class)
47 public class MemcachedStableReadExternalCacheTest {
48
49 private static final int DEFAULT_TTL = 6;
50
51 @Mock
52 private MemcachedClientIF memClient;
53
54 @Mock
55 private Future<Boolean> booleanFuture;
56
57 @Mock
58 private MetricsRecorder metricsRecorder;
59
60 private RequestContext requestContext = new DefaultRequestContext("tenant-id");
61 private StableReadExternalCache<String> cache;
62
63 @Before
64 public void init() {
65 final ExternalCacheSettings settings = new ExternalCacheSettingsBuilder()
66 .entryGrowthRateHint(ChangeRate.LOW_CHANGE)
67 .entryCountHint(5)
68 .defaultTtl(Duration.ofSeconds(DEFAULT_TTL))
69 .dataChangeRateHint(ChangeRate.HIGH_CHANGE)
70 .build();
71 cache = new MemcachedStableReadExternalCache<>(
72 () -> memClient,
73 () -> requestContext,
74 new PlainExternalCacheKeyGenerator("prodid"),
75 "mocked",
76 StringMarshalling.pair(),
77 settings,
78 metricsRecorder,
79 Duration.ofSeconds(5));
80 }
81
82 @Test
83 public void put_policy_set() throws Exception {
84 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
85 when(booleanFuture.get()).thenReturn(true);
86 when(memClient.set(
87 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
88 eq(new StringMarshalling().marshallToBytes("value"))))
89 .thenReturn(booleanFuture);
90
91 final CompletionStage<Boolean> f2 = cache.put("missing", "value", PutPolicy.PUT_ALWAYS);
92
93 assertThat(f2, successfulWith(is(true)));
94
95 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
96 verify(memClient).set(
97 eq("prodid::tenant-id::mocked::3::missing"), eq(DEFAULT_TTL),
98 eq(new StringMarshalling().marshallToBytes("value")));
99 verifyNoMoreInteractions(memClient);
100 }
101
102 @Test
103 public void getBulk_no_keys() throws ExecutionException, InterruptedException {
104 final CompletionStage<Map<String, Optional<String>>> get = cache.getBulk();
105
106 assertThat(get, successful());
107 assertThat(unsafeJoin(get).isEmpty(), is(true));
108 verifyNoMoreInteractions(memClient);
109 }
110
111 @Test
112 public void getBulk_function_no_keys() throws ExecutionException, InterruptedException {
113 final CompletionStage<Map<String, String>> get = cache.getBulk(keys -> null);
114
115 assertThat(get, successful());
116 assertThat(unsafeJoin(get).isEmpty(), is(true));
117 verifyNoMoreInteractions(memClient);
118 }
119
120 @Test
121 public void remove_no_keys() throws ExecutionException, InterruptedException {
122 final CompletionStage<Void> rm1 = cache.remove();
123
124 assertThat(rm1, successful());
125 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
126 verifyNoMoreInteractions(memClient);
127 }
128
129 @Test
130 public void removeAll_getBulk() throws Exception {
131 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
132 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L))).thenReturn(4L);
133
134 final CompletionStage<Void> rm1 = cache.removeAll();
135
136 assertThat(rm1, successful());
137 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
138 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L));
139 verifyNoMoreInteractions(memClient);
140
141 final HashSet<String> externalKeys = Sets.newHashSet(
142 "prodid::tenant-id::mocked::4::key-1", "prodid::tenant-id::mocked::4::key-2");
143 final Map<String, Object> externalResult = new HashMap<>();
144 externalResult.put("prodid::tenant-id::mocked::4::key-1", null);
145 externalResult.put("prodid::tenant-id::mocked::4::key-2", null);
146 when(memClient.getBulk(eq(externalKeys))).thenReturn(externalResult);
147
148 final CompletionStage<Map<String, Optional<String>>> get1 = cache.getBulk("key-1", "key-2");
149
150 assertThat(get1, successful());
151 assertThat(unsafeJoin(get1).keySet(), containsInAnyOrder("key-1", "key-2"));
152
153 assertThat(unsafeJoin(get1).values(), containsInAnyOrder(Optional.empty(), Optional.empty()));
154
155 verify(memClient).getBulk(eq(externalKeys));
156 verifyNoMoreInteractions(memClient);
157 }
158
159 @Test
160 public void removeAll_getBulkFactory() throws Exception {
161 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(3L);
162 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L))).thenReturn(4L);
163
164 final CompletionStage<Void> rm1 = cache.removeAll();
165
166 assertThat(rm1, successful());
167 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L));
168 verify(memClient).incr(eq("prodid::tenant-id::mocked::cache-version"), eq(1), eq(1L));
169 verifyNoMoreInteractions(memClient);
170
171 final HashSet<String> externalKeys = Sets.newHashSet(
172 "prodid::tenant-id::mocked::4::key-1", "prodid::tenant-id::mocked::4::key-2");
173 final Map<String, Object> externalResult = new HashMap<>();
174 externalResult.put(
175 "prodid::tenant-id::mocked::4::key-1",
176 new StringMarshalling().marshallToBytes("ice-monkeys"));
177 when(memClient.getBulk(eq(externalKeys))).thenReturn(externalResult);
178 when(memClient.add(
179 eq("prodid::tenant-id::mocked::4::key-2"),
180 eq(DEFAULT_TTL),
181 eq(new StringMarshalling().marshallToBytes("key-2-1")))
182 ).thenReturn(booleanFuture);
183 when(booleanFuture.get()).thenReturn(true);
184
185 final CompletionStage<Map<String, String>> get1 = cache.getBulk(
186 keys -> Maps.asMap(keys, k -> k + "-1"), "key-1", "key-2");
187
188 assertThat(get1, successful());
189 assertThat(unsafeJoin(get1).keySet(), containsInAnyOrder("key-1", "key-2"));
190 assertThat(unsafeJoin(get1).values(), containsInAnyOrder("ice-monkeys", "key-2-1"));
191
192 verify(memClient).getBulk(eq(externalKeys));
193 verify(memClient).add(
194 eq("prodid::tenant-id::mocked::4::key-2"),
195 eq(DEFAULT_TTL),
196 eq(new StringMarshalling().marshallToBytes("key-2-1")));
197 verifyNoMoreInteractions(memClient);
198 }
199
200 @Test
201 public void getBulk_metrics() throws Exception {
202 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(4L);
203
204 final HashSet<String> externalKeys = Sets.newHashSet(
205 "prodid::tenant-id::mocked::4::key-1", "prodid::tenant-id::mocked::4::key-2");
206 final Map<String, Object> externalResult = new HashMap<>();
207 externalResult.put(
208 "prodid::tenant-id::mocked::4::key-1",
209 new StringMarshalling().marshallToBytes("ice-monkeys"));
210 when(memClient.getBulk(eq(externalKeys))).thenReturn(externalResult);
211 when(memClient.add(
212 eq("prodid::tenant-id::mocked::4::key-2"),
213 eq(DEFAULT_TTL),
214 eq(new StringMarshalling().marshallToBytes("key-2-1")))
215 ).thenReturn(booleanFuture);
216 when(booleanFuture.get()).thenReturn(true);
217
218 final CompletionStage<Map<String, String>> get1 = cache.getBulk(
219 keys -> Maps.asMap(keys, k -> k + "-1"), "key-1", "key-2");
220
221 assertThat(get1, successful());
222 assertThat(unsafeJoin(get1).keySet(), containsInAnyOrder("key-1", "key-2"));
223 assertThat(unsafeJoin(get1).values(), containsInAnyOrder("ice-monkeys", "key-2-1"));
224
225 verify(metricsRecorder, times(2)).record(eq("mocked"), eq(CacheType.EXTERNAL), eq(MetricLabel.NUMBER_OF_REMOTE_GET), eq(1L));
226 verifyNoMoreInteractions(metricsRecorder);
227 }
228
229 @Test
230 public void get_metrics() throws Exception {
231 when(memClient.incr(eq("prodid::tenant-id::mocked::cache-version"), eq(0), eq(1L))).thenReturn(4L);
232 when(memClient.get(eq("prodid::tenant-id::mocked::4::key-1")))
233 .thenReturn(new StringMarshalling().marshallToBytes("it exists"));
234
235 final CompletionStage<String> get1 = cache.get("key-1", () -> "value-1");
236
237 assertThat(get1, successful());
238
239 verify(metricsRecorder).record(eq("mocked"), eq(CacheType.EXTERNAL), eq(MetricLabel.NUMBER_OF_REMOTE_GET), eq(1L));
240 verifyNoMoreInteractions(metricsRecorder);
241 }
242 }