• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package io.grpc.clientcacheexample;
2 
3 import com.google.common.truth.Truth;
4 
5 import static org.junit.Assert.assertEquals;
6 import static org.junit.Assert.assertNotEquals;
7 import static org.junit.Assert.assertSame;
8 import static org.junit.Assert.fail;
9 
10 import io.grpc.CallOptions;
11 import io.grpc.Channel;
12 import io.grpc.ClientInterceptors;
13 import io.grpc.ForwardingServerCall;
14 import io.grpc.ManagedChannel;
15 import io.grpc.Metadata;
16 import io.grpc.MethodDescriptor;
17 import io.grpc.ServerCall;
18 import io.grpc.ServerCallHandler;
19 import io.grpc.ServerInterceptor;
20 import io.grpc.ServerInterceptors;
21 import io.grpc.Status;
22 import io.grpc.StatusRuntimeException;
23 import io.grpc.examples.helloworld.AnotherGreeterGrpc;
24 import io.grpc.examples.helloworld.GreeterGrpc;
25 import io.grpc.examples.helloworld.HelloReply;
26 import io.grpc.examples.helloworld.HelloRequest;
27 import io.grpc.stub.ClientCalls;
28 import io.grpc.stub.StreamObserver;
29 import io.grpc.testing.GrpcServerRule;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.concurrent.TimeUnit;
35 import org.junit.After;
36 import org.junit.Before;
37 import org.junit.Rule;
38 import org.junit.Test;
39 
40 public class SafeMethodCachingInterceptorTest {
41   private static final Metadata.Key<String> CACHE_CONTROL_METADATA_KEY =
42       Metadata.Key.of("cache-control", Metadata.ASCII_STRING_MARSHALLER);
43 
44   @Rule public final GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor();
45 
46   private final GreeterGrpc.GreeterImplBase greeterServiceImpl =
47       new GreeterGrpc.GreeterImplBase() {
48         private int count = 1;
49 
50         @Override
51         public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
52           HelloReply reply =
53               HelloReply.newBuilder().setMessage("Hello " + req.getName() + " " + count++).build();
54           responseObserver.onNext(reply);
55           responseObserver.onCompleted();
56         }
57 
58         @Override
59         public void sayAnotherHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
60           HelloReply reply =
61               HelloReply.newBuilder()
62                   .setMessage("Hello again " + req.getName() + " " + count++)
63                   .build();
64           responseObserver.onNext(reply);
65           responseObserver.onCompleted();
66         }
67       };
68 
69   private final AnotherGreeterGrpc.AnotherGreeterImplBase anotherGreeterServiceImpl =
70       new AnotherGreeterGrpc.AnotherGreeterImplBase() {
71         private int count = 1;
72 
73         @Override
74         public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
75           HelloReply reply =
76               HelloReply.newBuilder().setMessage("Hey " + req.getName() + " " + count++).build();
77           responseObserver.onNext(reply);
78           responseObserver.onCompleted();
79         }
80       };
81 
82   private final List<String> cacheControlDirectives = new ArrayList<>();
83   private ServerInterceptor injectCacheControlInterceptor =
84       new ServerInterceptor() {
85         @Override
86         public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
87             ServerCall<ReqT, RespT> call,
88             final Metadata requestHeaders,
89             ServerCallHandler<ReqT, RespT> next) {
90           return next.startCall(
91               new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
92                 @Override
93                 public void sendHeaders(Metadata headers) {
94                   for (String cacheControlDirective : cacheControlDirectives) {
95                     headers.put(CACHE_CONTROL_METADATA_KEY, cacheControlDirective);
96                   }
97                   super.sendHeaders(headers);
98                 }
99               },
100               requestHeaders);
101         }
102       };
103 
104   private final HelloRequest message = HelloRequest.newBuilder().setName("Test Name").build();
105   private final MethodDescriptor<HelloRequest, HelloReply> safeGreeterSayHelloMethod =
106       GreeterGrpc.getSayHelloMethod().toBuilder().setSafe(true).build();
107   private final TestCache cache = new TestCache();
108 
109   private ManagedChannel baseChannel;
110   private Channel channelToUse;
111 
112   @Before
setUp()113   public void setUp() throws Exception {
114     grpcServerRule
115         .getServiceRegistry()
116         .addService(
117             ServerInterceptors.intercept(greeterServiceImpl, injectCacheControlInterceptor));
118     grpcServerRule.getServiceRegistry().addService(anotherGreeterServiceImpl);
119     baseChannel = grpcServerRule.getChannel();
120 
121     SafeMethodCachingInterceptor interceptor =
122         SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache);
123 
124     channelToUse = ClientInterceptors.intercept(baseChannel, interceptor);
125   }
126 
127   @After
tearDown()128   public void tearDown() {
129     baseChannel.shutdown();
130   }
131 
132   @Test
safeCallsAreCached()133   public void safeCallsAreCached() {
134     HelloReply reply1 =
135         ClientCalls.blockingUnaryCall(
136             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
137     HelloReply reply2 =
138         ClientCalls.blockingUnaryCall(
139             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
140 
141     assertSame(reply1, reply2);
142   }
143 
144   @Test
safeCallsAreCachedWithCopiedMethodDescriptor()145   public void safeCallsAreCachedWithCopiedMethodDescriptor() {
146     HelloReply reply1 =
147         ClientCalls.blockingUnaryCall(
148             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
149     HelloReply reply2 =
150         ClientCalls.blockingUnaryCall(
151             channelToUse,
152             safeGreeterSayHelloMethod.toBuilder().build(),
153             CallOptions.DEFAULT,
154             message);
155 
156     assertSame(reply1, reply2);
157   }
158 
159   @Test
requestWithNoCacheOptionSkipsCache()160   public void requestWithNoCacheOptionSkipsCache() {
161     HelloReply reply1 =
162         ClientCalls.blockingUnaryCall(
163             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
164     HelloReply reply2 =
165         ClientCalls.blockingUnaryCall(
166             channelToUse,
167             safeGreeterSayHelloMethod,
168             CallOptions.DEFAULT.withOption(SafeMethodCachingInterceptor.NO_CACHE_CALL_OPTION, true),
169             message);
170     HelloReply reply3 =
171         ClientCalls.blockingUnaryCall(
172             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
173 
174     assertNotEquals(reply1, reply2);
175     assertSame(reply1, reply3);
176   }
177 
178   @Test
requestWithOnlyIfCachedOption_unavailableIfNotInCache()179   public void requestWithOnlyIfCachedOption_unavailableIfNotInCache() {
180     try {
181       ClientCalls.blockingUnaryCall(
182           channelToUse,
183           safeGreeterSayHelloMethod,
184           CallOptions.DEFAULT.withOption(
185               SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true),
186           message);
187       fail("Expected call to fail");
188     } catch (StatusRuntimeException sre) {
189       assertEquals(Status.UNAVAILABLE.getCode(), sre.getStatus().getCode());
190       assertEquals(
191           "Unsatisfiable Request (only-if-cached set, but value not in cache)",
192           sre.getStatus().getDescription());
193     }
194   }
195 
196   @Test
requestWithOnlyIfCachedOption_usesCache()197   public void requestWithOnlyIfCachedOption_usesCache() {
198     HelloReply reply1 =
199         ClientCalls.blockingUnaryCall(
200             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
201     HelloReply reply2 =
202         ClientCalls.blockingUnaryCall(
203             channelToUse,
204             safeGreeterSayHelloMethod,
205             CallOptions.DEFAULT.withOption(
206                 SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true),
207             message);
208 
209     assertSame(reply1, reply2);
210   }
211 
212   @Test
requestWithNoCacheAndOnlyIfCached_fails()213   public void requestWithNoCacheAndOnlyIfCached_fails() {
214     try {
215       ClientCalls.blockingUnaryCall(
216           channelToUse,
217           safeGreeterSayHelloMethod,
218           CallOptions.DEFAULT
219               .withOption(SafeMethodCachingInterceptor.NO_CACHE_CALL_OPTION, true)
220               .withOption(SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true),
221           message);
222       fail("Expected call to fail");
223     } catch (StatusRuntimeException sre) {
224       assertEquals(Status.UNAVAILABLE.getCode(), sre.getStatus().getCode());
225       assertEquals(
226           "Unsatisfiable Request (no-cache and only-if-cached conflict)",
227           sre.getStatus().getDescription());
228     }
229   }
230 
231   @Test
responseNoCacheDirective_notCached()232   public void responseNoCacheDirective_notCached() throws Exception {
233     cacheControlDirectives.add("no-cache");
234 
235     HelloReply reply1 =
236         ClientCalls.blockingUnaryCall(
237             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
238     HelloReply reply2 =
239         ClientCalls.blockingUnaryCall(
240             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
241 
242     assertNotEquals(reply1, reply2);
243     assertNotEquals(reply1, reply2);
244     Truth.assertThat(cache.internalCache).isEmpty();
245     Truth.assertThat(cache.removedKeys).isEmpty();
246   }
247 
248   @Test
responseNoStoreDirective_notCached()249   public void responseNoStoreDirective_notCached() throws Exception {
250     cacheControlDirectives.add("no-store");
251 
252     HelloReply reply1 =
253         ClientCalls.blockingUnaryCall(
254             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
255     HelloReply reply2 =
256         ClientCalls.blockingUnaryCall(
257             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
258 
259     assertNotEquals(reply1, reply2);
260     Truth.assertThat(cache.internalCache).isEmpty();
261     Truth.assertThat(cache.removedKeys).isEmpty();
262   }
263 
264   @Test
responseNoTransformDirective_notCached()265   public void responseNoTransformDirective_notCached() throws Exception {
266     cacheControlDirectives.add("no-transform");
267 
268     HelloReply reply1 =
269         ClientCalls.blockingUnaryCall(
270             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
271     HelloReply reply2 =
272         ClientCalls.blockingUnaryCall(
273             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
274 
275     assertNotEquals(reply1, reply2);
276     Truth.assertThat(cache.internalCache).isEmpty();
277     Truth.assertThat(cache.removedKeys).isEmpty();
278   }
279 
280   @Test
responseMustRevalidateDirective_isIgnored()281   public void responseMustRevalidateDirective_isIgnored() throws Exception {
282     cacheControlDirectives.add("must-revalidate");
283 
284     HelloReply reply1 =
285         ClientCalls.blockingUnaryCall(
286             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
287     HelloReply reply2 =
288         ClientCalls.blockingUnaryCall(
289             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
290 
291     assertSame(reply1, reply2);
292   }
293 
294   @Test
responseMaxAge_caseInsensitive()295   public void responseMaxAge_caseInsensitive() throws Exception {
296     cacheControlDirectives.add("MaX-aGe=0");
297 
298     HelloReply reply1 =
299         ClientCalls.blockingUnaryCall(
300             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
301     HelloReply reply2 =
302         ClientCalls.blockingUnaryCall(
303             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
304 
305     assertNotEquals(reply1, reply2);
306     Truth.assertThat(cache.internalCache).isEmpty();
307     Truth.assertThat(cache.removedKeys).isEmpty();
308   }
309 
310   @Test
responseNoCache_caseInsensitive()311   public void responseNoCache_caseInsensitive() throws Exception {
312     cacheControlDirectives.add("No-CaCHe");
313 
314     HelloReply reply1 =
315         ClientCalls.blockingUnaryCall(
316             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
317     HelloReply reply2 =
318         ClientCalls.blockingUnaryCall(
319             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
320 
321     assertNotEquals(reply1, reply2);
322     Truth.assertThat(cache.internalCache).isEmpty();
323     Truth.assertThat(cache.removedKeys).isEmpty();
324   }
325 
326   @Test
combinedResponseCacheControlDirectives_parsesWithoutError()327   public void combinedResponseCacheControlDirectives_parsesWithoutError() throws Exception {
328     cacheControlDirectives.add("max-age=1,no-store , no-cache");
329 
330     HelloReply reply1 =
331         ClientCalls.blockingUnaryCall(
332             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
333     HelloReply reply2 =
334         ClientCalls.blockingUnaryCall(
335             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
336 
337     assertNotEquals(reply1, reply2);
338     Truth.assertThat(cache.internalCache).isEmpty();
339     Truth.assertThat(cache.removedKeys).isEmpty();
340   }
341 
342   @Test
separateResponseCacheControlDirectives_parsesWithoutError()343   public void separateResponseCacheControlDirectives_parsesWithoutError() throws Exception {
344     cacheControlDirectives.add("max-age=1");
345     cacheControlDirectives.add("no-store , no-cache");
346 
347     HelloReply reply1 =
348         ClientCalls.blockingUnaryCall(
349             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
350     HelloReply reply2 =
351         ClientCalls.blockingUnaryCall(
352             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
353 
354     assertNotEquals(reply1, reply2);
355     Truth.assertThat(cache.internalCache).isEmpty();
356     Truth.assertThat(cache.removedKeys).isEmpty();
357   }
358 
359   @Test
afterResponseMaxAge_cacheEntryInvalidated()360   public void afterResponseMaxAge_cacheEntryInvalidated() throws Exception {
361     cacheControlDirectives.add("max-age=1");
362 
363     HelloReply reply1 =
364         ClientCalls.blockingUnaryCall(
365             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
366     HelloReply reply2 =
367         ClientCalls.blockingUnaryCall(
368             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
369     assertSame(reply1, reply2);
370 
371     // Wait for cache entry to expire
372     sleepAtLeast(1001);
373 
374     assertNotEquals(
375         reply1,
376         ClientCalls.blockingUnaryCall(
377             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message));
378     Truth.assertThat(cache.removedKeys).hasSize(1);
379     assertEquals(
380         new SafeMethodCachingInterceptor.Key(
381             GreeterGrpc.getSayHelloMethod().getFullMethodName(), message),
382         cache.removedKeys.get(0));
383   }
384 
385   @Test
invalidResponseMaxAge_usesDefault()386   public void invalidResponseMaxAge_usesDefault() throws Exception {
387     SafeMethodCachingInterceptor interceptorWithCustomMaxAge =
388         SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache, 1);
389     channelToUse = ClientInterceptors.intercept(baseChannel, interceptorWithCustomMaxAge);
390     cacheControlDirectives.add("max-age=-10");
391 
392     HelloReply reply1 =
393         ClientCalls.blockingUnaryCall(
394             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
395     HelloReply reply2 =
396         ClientCalls.blockingUnaryCall(
397             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
398     assertEquals(reply1, reply2);
399 
400     // Wait for cache entry to expire
401     sleepAtLeast(1001);
402 
403     assertNotEquals(
404         reply1,
405         ClientCalls.blockingUnaryCall(
406             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message));
407     Truth.assertThat(cache.removedKeys).hasSize(1);
408     assertEquals(
409         new SafeMethodCachingInterceptor.Key(
410             GreeterGrpc.getSayHelloMethod().getFullMethodName(), message),
411         cache.removedKeys.get(0));
412   }
413 
414   @Test
responseMaxAgeZero_notAddedToCache()415   public void responseMaxAgeZero_notAddedToCache() throws Exception {
416     cacheControlDirectives.add("max-age=0");
417 
418     HelloReply reply1 =
419         ClientCalls.blockingUnaryCall(
420             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
421     HelloReply reply2 =
422         ClientCalls.blockingUnaryCall(
423             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
424 
425     assertNotEquals(reply1, reply2);
426     Truth.assertThat(cache.internalCache).isEmpty();
427     Truth.assertThat(cache.removedKeys).isEmpty();
428   }
429 
430   @Test
cacheHit_doesNotResetExpiration()431   public void cacheHit_doesNotResetExpiration() throws Exception {
432     cacheControlDirectives.add("max-age=1");
433 
434     HelloReply reply1 =
435         ClientCalls.blockingUnaryCall(
436             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
437     HelloReply reply2 =
438         ClientCalls.blockingUnaryCall(
439             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
440 
441     sleepAtLeast(1001);
442 
443     HelloReply reply3 =
444         ClientCalls.blockingUnaryCall(
445             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
446 
447     assertSame(reply1, reply2);
448     assertNotEquals(reply1, reply3);
449     Truth.assertThat(cache.internalCache).hasSize(1);
450     Truth.assertThat(cache.removedKeys).hasSize(1);
451   }
452 
453   @Test
afterDefaultMaxAge_cacheEntryInvalidated()454   public void afterDefaultMaxAge_cacheEntryInvalidated() throws Exception {
455     SafeMethodCachingInterceptor interceptorWithCustomMaxAge =
456         SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache, 1);
457     channelToUse = ClientInterceptors.intercept(baseChannel, interceptorWithCustomMaxAge);
458 
459     HelloReply reply1 =
460         ClientCalls.blockingUnaryCall(
461             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
462     HelloReply reply2 =
463         ClientCalls.blockingUnaryCall(
464             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
465     assertSame(reply1, reply2);
466 
467     // Wait for cache entry to expire
468     sleepAtLeast(1001);
469 
470     assertNotEquals(
471         reply1,
472         ClientCalls.blockingUnaryCall(
473             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message));
474     Truth.assertThat(cache.removedKeys).hasSize(1);
475     assertEquals(
476         new SafeMethodCachingInterceptor.Key(
477             GreeterGrpc.getSayHelloMethod().getFullMethodName(), message),
478         cache.removedKeys.get(0));
479   }
480 
481   @Test
unsafeCallsAreNotCached()482   public void unsafeCallsAreNotCached() {
483     GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channelToUse);
484 
485     HelloReply reply1 = stub.sayHello(message);
486     HelloReply reply2 = stub.sayHello(message);
487 
488     assertNotEquals(reply1, reply2);
489   }
490 
491   @Test
differentMethodCallsAreNotConflated()492   public void differentMethodCallsAreNotConflated() {
493     MethodDescriptor<HelloRequest, HelloReply> anotherSafeMethod =
494         GreeterGrpc.getSayAnotherHelloMethod().toBuilder().setSafe(true).build();
495 
496     HelloReply reply1 =
497         ClientCalls.blockingUnaryCall(
498             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
499     HelloReply reply2 =
500         ClientCalls.blockingUnaryCall(
501             channelToUse, anotherSafeMethod, CallOptions.DEFAULT, message);
502 
503     assertNotEquals(reply1, reply2);
504   }
505 
506   @Test
differentServiceCallsAreNotConflated()507   public void differentServiceCallsAreNotConflated() {
508     MethodDescriptor<HelloRequest, HelloReply> anotherSafeMethod =
509         AnotherGreeterGrpc.getSayHelloMethod().toBuilder().setSafe(true).build();
510 
511     HelloReply reply1 =
512         ClientCalls.blockingUnaryCall(
513             channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message);
514     HelloReply reply2 =
515         ClientCalls.blockingUnaryCall(
516             channelToUse, anotherSafeMethod, CallOptions.DEFAULT, message);
517 
518     assertNotEquals(reply1, reply2);
519   }
520 
sleepAtLeast(long millis)521   private static void sleepAtLeast(long millis) throws InterruptedException {
522     long delay = TimeUnit.MILLISECONDS.toNanos(millis);
523     long end = System.nanoTime() + delay;
524     while (delay > 0) {
525       TimeUnit.NANOSECONDS.sleep(delay);
526       delay = end - System.nanoTime();
527     }
528   }
529 
530   private static class TestCache implements SafeMethodCachingInterceptor.Cache {
531     private Map<SafeMethodCachingInterceptor.Key, SafeMethodCachingInterceptor.Value>
532         internalCache =
533             new HashMap<SafeMethodCachingInterceptor.Key, SafeMethodCachingInterceptor.Value>();
534     private List<SafeMethodCachingInterceptor.Key> removedKeys =
535         new ArrayList<SafeMethodCachingInterceptor.Key>();
536 
537     @Override
put( SafeMethodCachingInterceptor.Key key, SafeMethodCachingInterceptor.Value value)538     public void put(
539         SafeMethodCachingInterceptor.Key key, SafeMethodCachingInterceptor.Value value) {
540       internalCache.put(key, value);
541     }
542 
543     @Override
get(SafeMethodCachingInterceptor.Key key)544     public SafeMethodCachingInterceptor.Value get(SafeMethodCachingInterceptor.Key key) {
545       return internalCache.get(key);
546     }
547 
548     @Override
remove(SafeMethodCachingInterceptor.Key key)549     public void remove(SafeMethodCachingInterceptor.Key key) {
550       removedKeys.add(key);
551       internalCache.remove(key);
552     }
553 
554     @Override
clear()555     public void clear() {}
556   }
557 }
558