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