View Javadoc

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