From f954e5f3f34e71152cb6eb23a1484f9d49a5e522 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 30 Mar 2023 09:22:42 +0300 Subject: [PATCH] Ensure redis-cache does not cause load value on the wrong executor --- .../cache/redis/runtime/RedisCacheImpl.java | 54 +++++++++--- .../redis/runtime/RedisCacheImplTest.java | 43 ++++----- integration-tests/redis-cache/pom.xml | 17 ++++ .../it/cache/redis/RestClientResource.java | 88 +++++++++++++++++++ .../it/cache/redis/SunriseRestClient.java | 52 +++++++++++ .../redis/SunriseRestServerResource.java | 41 +++++++++ .../src/main/resources/application.properties | 3 + .../it/cache/redis/RestClientTestCase.java | 86 ++++++++++++++++++ 8 files changed, 353 insertions(+), 31 deletions(-) create mode 100644 integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/RestClientResource.java create mode 100644 integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestClient.java create mode 100644 integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestServerResource.java create mode 100644 integration-tests/redis-cache/src/test/java/io/quarkus/it/cache/redis/RestClientTestCase.java diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java index 0fa03e1c9e961..a3624e424026c 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java @@ -16,9 +16,12 @@ import io.quarkus.cache.runtime.AbstractCache; import io.quarkus.redis.client.RedisClientName; import io.quarkus.redis.runtime.datasource.Marshaller; +import io.quarkus.runtime.BlockingOperationControl; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; import io.smallrye.mutiny.unchecked.UncheckedFunction; +import io.smallrye.mutiny.vertx.MutinyHelper; +import io.vertx.mutiny.core.Vertx; import io.vertx.mutiny.redis.client.Command; import io.vertx.mutiny.redis.client.Redis; import io.vertx.mutiny.redis.client.RedisConnection; @@ -41,6 +44,7 @@ public class RedisCacheImpl extends AbstractCache implements RedisCache { "double", Double.class, "boolean", Boolean.class); + private final Vertx vertx; private final Redis redis; private final RedisCacheInfo cacheInfo; @@ -49,9 +53,12 @@ public class RedisCacheImpl extends AbstractCache implements RedisCache { private final Marshaller marshaller; + private final Supplier blockingAllowedSupplier; + public RedisCacheImpl(RedisCacheInfo cacheInfo, Optional redisClientName) { - this(cacheInfo, determineRedisClient(redisClientName)); + this(cacheInfo, Arc.container().select(Vertx.class).get(), determineRedisClient(redisClientName), + BlockingOperationControl::isBlockingAllowed); } private static Redis determineRedisClient(Optional redisClientName) { @@ -64,8 +71,10 @@ private static Redis determineRedisClient(Optional redisClientName) { } @SuppressWarnings("unchecked") - public RedisCacheImpl(RedisCacheInfo cacheInfo, Redis redis) { + public RedisCacheImpl(RedisCacheInfo cacheInfo, Vertx vertx, Redis redis, Supplier blockingAllowedSupplier) { + this.vertx = vertx; this.cacheInfo = cacheInfo; + this.blockingAllowedSupplier = blockingAllowedSupplier; try { this.classOfKey = (Class) loadClass(this.cacheInfo.keyType); @@ -127,6 +136,7 @@ public Uni get(K key, Class clazz, Function valueLoader) { // val = deserialize(GET K) // if (val == null) => SET K computation.apply(K) byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key))); + boolean isWorkerThread = blockingAllowedSupplier.get(); return withConnection(new Function>() { @Override public Uni apply(RedisConnection connection) { @@ -138,17 +148,39 @@ public Uni apply(V cached) throws Exception { if (cached != null) { return Uni.createFrom().item(new StaticSupplier<>(cached)); } else { - V value = valueLoader.apply(key); - if (value == null) { - throw new IllegalArgumentException("Cannot cache `null` value"); - } - byte[] encodedValue = marshaller.encode(value); - if (cacheInfo.useOptimisticLocking) { - return multi(connection, set(connection, encodedKey, encodedValue)) - .replaceWith(value); + Uni uni; + if (isWorkerThread) { + uni = Uni.createFrom().item(new Supplier() { + @Override + public V get() { + return valueLoader.apply(key); + } + }).runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate())); } else { - return set(connection, encodedKey, encodedValue).replaceWith(value); + uni = Uni.createFrom().item(valueLoader.apply(key)); } + + return uni.onItem().call(new Function>() { + @Override + public Uni apply(V value) { + if (value == null) { + throw new IllegalArgumentException("Cannot cache `null` value"); + } + byte[] encodedValue = marshaller.encode(value); + Uni result; + if (cacheInfo.useOptimisticLocking) { + result = multi(connection, set(connection, encodedKey, encodedValue)) + .replaceWith(value); + } else { + result = set(connection, encodedKey, encodedValue).replaceWith(value); + } + if (isWorkerThread) { + return result + .runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate())); + } + return result; + } + }); } } })); diff --git a/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java b/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java index b29be5d993ea6..ed5ad172528d2 100644 --- a/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java +++ b/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java @@ -7,6 +7,7 @@ import java.time.Duration; import java.util.*; +import java.util.function.Supplier; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -20,6 +21,8 @@ class RedisCacheImplTest extends RedisCacheTestBase { + private static final Supplier BLOCKING_ALLOWED = () -> false; + @AfterEach void clear() { redis.send(Request.cmd(Command.FLUSHALL).arg("SYNC")).await().atMost(Duration.ofSeconds(10)); @@ -32,7 +35,7 @@ public void testPutInTheCache() { info.name = "foo"; info.valueType = String.class.getName(); info.ttl = Optional.of(Duration.ofSeconds(2)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThat(cache.get(k, s -> "hello").await().indefinitely()).isEqualTo("hello"); var r = redis.send(Request.cmd(Command.GET).arg("cache:foo:" + k)).await().indefinitely(); assertThat(r).isNotNull(); @@ -46,7 +49,7 @@ public void testPutInTheCacheWithOptimisitcLocking() { info.valueType = String.class.getName(); info.ttl = Optional.of(Duration.ofSeconds(2)); info.useOptimisticLocking = true; - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThat(cache.get(k, s -> "hello").await().indefinitely()).isEqualTo("hello"); var r = redis.send(Request.cmd(Command.GET).arg("cache:foo:" + k)).await().indefinitely(); assertThat(r).isNotNull(); @@ -58,7 +61,7 @@ public void testPutAndWaitForInvalidation() { RedisCacheInfo info = new RedisCacheInfo(); info.valueType = String.class.getName(); info.ttl = Optional.of(Duration.ofSeconds(1)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThat(cache.get(k, s -> "hello").await().indefinitely()).isEqualTo("hello"); var x = cache.get(k, String::toUpperCase).await().indefinitely(); assertEquals(x, "hello"); @@ -70,7 +73,7 @@ public void testManualInvalidation() { RedisCacheInfo info = new RedisCacheInfo(); info.valueType = String.class.getName(); info.ttl = Optional.of(Duration.ofSeconds(10)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); cache.get("foo", s -> "hello").await().indefinitely(); var x = cache.get("foo", String::toUpperCase).await().indefinitely(); assertEquals(x, "hello"); @@ -113,7 +116,7 @@ public void testGetOrNull() { RedisCacheInfo info = new RedisCacheInfo(); info.ttl = Optional.of(Duration.ofSeconds(10)); info.valueType = Person.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); Person person = cache.getOrNull("foo", Person.class).await().indefinitely(); assertThat(person).isNull(); assertThatTheKeyDoesNotExist("cache:foo"); @@ -133,7 +136,7 @@ public void testGetOrDefault() { RedisCacheInfo info = new RedisCacheInfo(); info.ttl = Optional.of(Duration.ofSeconds(10)); info.valueType = Person.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); Person person = cache.getOrDefault("foo", new Person("bar", "BAR")).await().indefinitely(); assertThat(person).isNotNull() .satisfies(p -> { @@ -159,7 +162,7 @@ public void testCacheNullValue() { RedisCacheInfo info = new RedisCacheInfo(); info.ttl = Optional.of(Duration.ofSeconds(10)); info.valueType = Person.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); // with custom key double key = 122334545.0; @@ -175,7 +178,7 @@ public void testExceptionInValueLoader() { info.valueType = Person.class.getName(); info.keyType = Double.class.getName(); info.name = "foo"; - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); // with custom key and exception Double key = 122334545.0; @@ -197,7 +200,7 @@ public void testPutShouldPopulateCache() { info.ttl = Optional.of(Duration.ofSeconds(10)); info.valueType = Person.class.getName(); info.keyType = Integer.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); cache.put(1, new Person("luke", "skywalker")).await().indefinitely(); assertThat(cache.get(1, x -> new Person("1", "1")).await().indefinitely()).isEqualTo(new Person("luke", "skywalker")); @@ -214,7 +217,7 @@ public void testPutShouldPopulateCacheWithOptimisticLocking() { info.valueType = Person.class.getName(); info.keyType = Integer.class.getName(); info.useOptimisticLocking = true; - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); cache.put(1, new Person("luke", "skywalker")).await().indefinitely(); assertThat(cache.get(1, x -> new Person("1", "1")).await().indefinitely()).isEqualTo(new Person("luke", "skywalker")); @@ -231,7 +234,7 @@ public void testThatConnectionsAreRecycled() { info.name = "foo"; info.valueType = String.class.getName(); info.ttl = Optional.of(Duration.ofSeconds(1)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); for (int i = 0; i < 1000; i++) { String val = "hello-" + i; @@ -257,7 +260,7 @@ public void testThatConnectionsAreRecycledWithOptimisticLocking() { info.valueType = String.class.getName(); info.ttl = Optional.of(Duration.ofSeconds(1)); info.useOptimisticLocking = true; - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); for (int i = 0; i < 1000; i++) { String val = "hello-" + i; @@ -280,7 +283,7 @@ void testWithMissingDefaultType() { RedisCacheInfo info = new RedisCacheInfo(); info.name = "missing-default-cache"; info.ttl = Optional.of(Duration.ofSeconds(10)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThatThrownBy(() -> cache.get("test", x -> "value").await().indefinitely()) .isInstanceOf(UnsupportedOperationException.class); @@ -303,7 +306,7 @@ void testAsyncGetWithDefaultType() { info.name = "star-wars"; info.ttl = Optional.of(Duration.ofSeconds(2)); info.valueType = Person.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThat(cache .getAsync("test", @@ -334,7 +337,7 @@ void testAsyncGetWithDefaultTypeWithOptimisticLocking() { info.ttl = Optional.of(Duration.ofSeconds(2)); info.valueType = Person.class.getName(); info.useOptimisticLocking = true; - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThat(cache .getAsync("test", @@ -364,7 +367,7 @@ void testPut() { info.name = "put"; info.ttl = Optional.of(Duration.ofSeconds(2)); info.valueType = Person.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); Person luke = new Person("luke", "skywalker"); Person leia = new Person("leia", "organa"); @@ -383,7 +386,7 @@ void testPutWithSupplier() { info.name = "put"; info.ttl = Optional.of(Duration.ofSeconds(2)); info.valueType = Person.class.getName(); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); Person luke = new Person("luke", "skywalker"); Person leia = new Person("leia", "organa"); @@ -403,7 +406,7 @@ void testInitializationWithAnUnknownClass() { info.ttl = Optional.of(Duration.ofSeconds(2)); info.valueType = Person.class.getPackage().getName() + ".Missing"; - assertThatThrownBy(() -> new RedisCacheImpl<>(info, redis)) + assertThatThrownBy(() -> new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED)) .isInstanceOf(IllegalArgumentException.class); } @@ -413,7 +416,7 @@ void testGetDefaultKey() { info.name = "test-default-key"; info.ttl = Optional.of(Duration.ofSeconds(2)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); assertThat(cache.getDefaultKey()).isEqualTo("default-cache-key"); assertThat(cache.getDefaultValueType()).isNull(); @@ -425,7 +428,7 @@ void testInvalidation() { info.name = "test-invalidation"; info.ttl = Optional.of(Duration.ofSeconds(10)); - RedisCacheImpl cache = new RedisCacheImpl<>(info, redis); + RedisCacheImpl cache = new RedisCacheImpl<>(info, vertx, redis, BLOCKING_ALLOWED); redis.send(Request.cmd(Command.SET).arg("key6").arg("my-value")).await().indefinitely(); diff --git a/integration-tests/redis-cache/pom.xml b/integration-tests/redis-cache/pom.xml index fdd4ad906046e..0043638f2bdd0 100644 --- a/integration-tests/redis-cache/pom.xml +++ b/integration-tests/redis-cache/pom.xml @@ -20,6 +20,10 @@ io.quarkus quarkus-resteasy-reactive-jackson + + io.quarkus + quarkus-rest-client-reactive-jackson + io.quarkus quarkus-redis-cache @@ -64,6 +68,19 @@ + + io.quarkus + quarkus-rest-client-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/RestClientResource.java b/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/RestClientResource.java new file mode 100644 index 0000000000000..edef7d8edd43d --- /dev/null +++ b/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/RestClientResource.java @@ -0,0 +1,88 @@ +package io.quarkus.it.cache.redis; + +import java.util.Set; +import java.util.function.Function; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.HttpHeaders; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.RestResponse; + +import io.quarkus.runtime.BlockingOperationControl; +import io.smallrye.mutiny.Uni; + +@Path("rest-client") +public class RestClientResource { + + @RestClient + SunriseRestClient sunriseRestClient; + + @Inject + HttpHeaders headers; // used in order to make sure that @RequestScoped beans continue to work despite the cache coming into play + + @GET + @Path("time/{city}") + public RestResponse getSunriseTime(@RestPath String city, @RestQuery String date) { + Set incomingHeadersBeforeRestCall = headers.getRequestHeaders().keySet(); + String restResponse = sunriseRestClient.getSunriseTime(city, date); + Set incomingHeadersAfterRestCall = headers.getRequestHeaders().keySet(); + return RestResponse.ResponseBuilder + .ok(restResponse) + .header("before", String.join(", ", incomingHeadersBeforeRestCall)) + .header("after", String.join(", ", incomingHeadersAfterRestCall)) + .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed()) + .build(); + } + + @GET + @Path("async/time/{city}") + public Uni> getAsyncSunriseTime(@RestPath String city, @RestQuery String date) { + Set incomingHeadersBeforeRestCall = headers.getRequestHeaders().keySet(); + return sunriseRestClient.getAsyncSunriseTime(city, date).onItem().transform(new Function<>() { + @Override + public RestResponse apply(String restResponse) { + Set incomingHeadersAfterRestCall = headers.getRequestHeaders().keySet(); + return RestResponse.ResponseBuilder + .ok(restResponse) + .header("before", String.join(", ", incomingHeadersBeforeRestCall)) + .header("after", String.join(", ", incomingHeadersAfterRestCall)) + .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed()) + .build(); + } + }); + } + + @GET + @Path("invocations") + public Integer getSunriseTimeInvocations() { + return sunriseRestClient.getSunriseTimeInvocations(); + } + + @DELETE + @Path("invalidate/{city}") + public Uni> invalidate(@RestPath String city, @RestQuery String notPartOfTheCacheKey, + @RestQuery String date) { + return sunriseRestClient.invalidate(city, notPartOfTheCacheKey, date).onItem().transform( + new Function<>() { + @Override + public RestResponse apply(Void unused) { + return RestResponse.ResponseBuilder. create(RestResponse.Status.NO_CONTENT) + .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed()) + .header("incoming", String.join(", ", headers.getRequestHeaders().keySet())) + .build(); + } + }); + } + + @DELETE + @Path("invalidate") + public void invalidateAll() { + sunriseRestClient.invalidateAll(); + } +} diff --git a/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestClient.java b/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestClient.java new file mode 100644 index 0000000000000..e9123640691c7 --- /dev/null +++ b/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestClient.java @@ -0,0 +1,52 @@ +package io.quarkus.it.cache.redis; + +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; +import io.smallrye.mutiny.Uni; + +@RegisterRestClient +@Path("sunrise") +public interface SunriseRestClient { + + String CACHE_NAME = "sunrise-cache"; + + @GET + @Path("time/{city}") + @CacheResult(cacheName = CACHE_NAME) + String getSunriseTime(@RestPath String city, @RestQuery String date); + + @GET + @Path("time/{city}") + @CacheResult(cacheName = CACHE_NAME) + Uni getAsyncSunriseTime(@RestPath String city, @RestQuery String date); + + @GET + @Path("invocations") + Integer getSunriseTimeInvocations(); + + /* + * The following methods wouldn't make sense in a real-life application but it's not relevant here. We only need to check if + * the caching annotations work as intended with the rest-client extension. + */ + + @DELETE + @Path("invalidate/{city}") + @CacheInvalidate(cacheName = CACHE_NAME) + Uni invalidate(@CacheKey @RestPath String city, @RestQuery String notPartOfTheCacheKey, + @CacheKey @RestPath String date); + + @DELETE + @Path("invalidate") + @CacheInvalidateAll(cacheName = CACHE_NAME) + void invalidateAll(); +} diff --git a/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestServerResource.java b/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestServerResource.java new file mode 100644 index 0000000000000..d8f30af1586fc --- /dev/null +++ b/integration-tests/redis-cache/src/main/java/io/quarkus/it/cache/redis/SunriseRestServerResource.java @@ -0,0 +1,41 @@ +package io.quarkus.it.cache.redis; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +@ApplicationScoped +@Path("sunrise") +public class SunriseRestServerResource { + + private int sunriseTimeInvocations; + + @GET + @Path("time/{city}") + public String getSunriseTime(@RestPath String city, @RestQuery String date) { + sunriseTimeInvocations++; + return "2020-12-20T10:15:30"; + } + + @GET + @Path("invocations") + public Integer getSunriseTimeInvocations() { + return sunriseTimeInvocations; + } + + @DELETE + @Path("invalidate/{city}") + public void invalidate(@RestPath String city, @RestQuery String notPartOfTheCacheKey, @RestQuery String date) { + // Do nothing. We only need to test the caching annotation on the client side. + } + + @DELETE + @Path("invalidate") + public void invalidateAll() { + // Do nothing. We only need to test the caching annotation on the client side. + } +} diff --git a/integration-tests/redis-cache/src/main/resources/application.properties b/integration-tests/redis-cache/src/main/resources/application.properties index b855ea5dfadfe..df31c279e5404 100644 --- a/integration-tests/redis-cache/src/main/resources/application.properties +++ b/integration-tests/redis-cache/src/main/resources/application.properties @@ -1 +1,4 @@ quarkus.redis.hosts=redis://localhost:6379/0 +# needed only because we use the same cache for both String and Uni results +quarkus.cache.redis.sunrise-cache.value-type=java.lang.String +io.quarkus.it.cache.redis.SunriseRestClient/mp-rest/url=${test.url} diff --git a/integration-tests/redis-cache/src/test/java/io/quarkus/it/cache/redis/RestClientTestCase.java b/integration-tests/redis-cache/src/test/java/io/quarkus/it/cache/redis/RestClientTestCase.java new file mode 100644 index 0000000000000..21f3e18942d92 --- /dev/null +++ b/integration-tests/redis-cache/src/test/java/io/quarkus/it/cache/redis/RestClientTestCase.java @@ -0,0 +1,86 @@ +package io.quarkus.it.cache.redis; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.Headers; + +@QuarkusTest +@DisplayName("Tests the integration between the cache and the rest-client extensions") +public class RestClientTestCase { + + private static final String CITY = "Toulouse"; + private static final String TODAY = "2020-12-20"; + + @Test + public void test() { + assertInvocations("0"); + getSunriseTimeInvocations(); + assertInvocations("1"); + getSunriseTimeInvocations(); + assertInvocations("1"); + getAsyncSunriseTimeInvocations(); + assertInvocations("1"); + invalidate(); + getSunriseTimeInvocations(); + assertInvocations("2"); + invalidateAll(); + getSunriseTimeInvocations(); + assertInvocations("3"); + } + + private void assertInvocations(String expectedInvocations) { + given() + .when() + .get("/rest-client/invocations") + .then() + .statusCode(200) + .body(equalTo(expectedInvocations)); + } + + private void getSunriseTimeInvocations() { + doGetSunriseTimeInvocations("/rest-client/time/{city}", true); + } + + private void getAsyncSunriseTimeInvocations() { + doGetSunriseTimeInvocations("/rest-client/async/time/{city}", false); + } + + private void doGetSunriseTimeInvocations(String path, Boolean blockingAllowed) { + Headers headers = given() + .queryParam("date", TODAY) + .when() + .get(path, CITY) + .then() + .statusCode(200) + .extract().headers(); + assertEquals(headers.get("before").getValue(), headers.get("after").getValue()); + assertEquals(blockingAllowed.toString(), headers.get("blockingAllowed").getValue()); + } + + private void invalidate() { + Headers headers = given() + .queryParam("date", TODAY) + .queryParam("notPartOfTheCacheKey", "notPartOfTheCacheKey") + .when() + .delete("/rest-client/invalidate/{city}", CITY) + .then() + .statusCode(204) + .extract().headers(); + assertNotNull(headers.get("incoming").getValue()); + assertEquals("false", headers.get("blockingAllowed").getValue()); + } + + private void invalidateAll() { + given() + .when() + .delete("/rest-client/invalidate") + .then() + .statusCode(204); + } +}