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