From 713d3240dcae44225a569a95cf8a45ca0e1e9acc Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Thu, 16 Nov 2023 20:22:39 +0100 Subject: [PATCH 1/2] Add tests for redis cache and coverage for quarkusio/quarkus#35680 Most of the tests are reused from caffeine module anshould cover similar scenarios as spring che and caffeine cache. --- README.md | 9 + cache/redis/pom.xml | 36 ++++ .../cache/redis/ApplicationScopeService.java | 7 + .../ts/cache/redis/BaseServiceWithCache.java | 38 ++++ .../ts/cache/redis/RequestScopeService.java | 7 + .../cache/redis/ServiceWithCacheResource.java | 65 ++++++ .../ts/cache/redis/TemplateCacheResource.java | 31 +++ .../src/main/resources/application.properties | 0 .../src/main/resources/templates/cached.html | 1 + .../ts/cache/redis/OpenShiftRedisCacheIT.java | 7 + .../quarkus/ts/cache/redis/RedisCacheIT.java | 185 ++++++++++++++++++ .../redis/src/test/resources/test.properties | 1 + pom.xml | 1 + 13 files changed, 388 insertions(+) create mode 100644 cache/redis/pom.xml create mode 100644 cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java create mode 100644 cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java create mode 100644 cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java create mode 100644 cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java create mode 100644 cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java create mode 100644 cache/redis/src/main/resources/application.properties create mode 100644 cache/redis/src/main/resources/templates/cached.html create mode 100644 cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java create mode 100644 cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java create mode 100644 cache/redis/src/test/resources/test.properties diff --git a/README.md b/README.md index d7d594899..3f7a7505a 100644 --- a/README.md +++ b/README.md @@ -1102,6 +1102,15 @@ It covers different usages: 3. from a blocking endpoint 4. from a reactive endpoint +### `cache/redis` + +Verifies the `quarkus-redis-cache` extension using `@CacheResult`, `@CacheInvalidate`, `@CacheInvalidateAll` and `@CacheKey`. +It covers different usages: +1. from an application scoped service +2. from a request scoped service + +Also verify that Qute correctly indicate that does not work with remote cache. + ### `cache/spring` Verifies the `quarkus-spring-cache` extension using `@Cacheable`, `@CacheEvict` and `@CachePut`. diff --git a/cache/redis/pom.xml b/cache/redis/pom.xml new file mode 100644 index 000000000..51f1e140e --- /dev/null +++ b/cache/redis/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../.. + + cache-redis + jar + Quarkus QE TS: Cache: Redis + + + io.quarkus + quarkus-cache + + + io.quarkus + quarkus-redis-cache + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-qute + + Added dependency to check https://github.com/quarkusio/quarkus/issues/35680 <--> + + io.quarkus + quarkus-mailer + + + diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java new file mode 100644 index 000000000..baf9fbebb --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ApplicationScopeService extends BaseServiceWithCache { +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java new file mode 100644 index 000000000..c93a2e55d --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java @@ -0,0 +1,38 @@ +package io.quarkus.ts.cache.caffeine; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; + +public abstract class BaseServiceWithCache { + + private static final String CACHE_NAME = "service-cache"; + + private static int counter = 0; + + @CacheResult(cacheName = CACHE_NAME) + public String getValue() { + return "Value: " + counter++; + } + + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidate() { + // do nothing + } + + @CacheResult(cacheName = CACHE_NAME) + public String getValueWithPrefix(@CacheKey String prefix) { + return prefix + ": " + counter++; + } + + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidateWithPrefix(@CacheKey String prefix) { + // do nothing + } + + @CacheInvalidateAll(cacheName = CACHE_NAME) + public void invalidateAll() { + // do nothing + } +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java new file mode 100644 index 000000000..8e12392b5 --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.enterprise.context.RequestScoped; + +@RequestScoped +public class RequestScopeService extends BaseServiceWithCache { +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java new file mode 100644 index 000000000..e71870276 --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java @@ -0,0 +1,65 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/services") +public class ServiceWithCacheResource { + + public static final String APPLICATION_SCOPE_SERVICE_PATH = "application-scope"; + public static final String REQUEST_SCOPE_SERVICE_PATH = "request-scope"; + + @Inject + ApplicationScopeService applicationScopeService; + + @Inject + RequestScopeService requestScopeService; + + @GET + @Path("/{service}") + @Produces(MediaType.TEXT_PLAIN) + public String getValueFromService(@PathParam("service") String service) { + return lookupServiceByPathParam(service).getValue(); + } + + @POST + @Path("/{service}/invalidate-cache") + public void invalidateCacheFromService(@PathParam("service") String service) { + lookupServiceByPathParam(service).invalidate(); + } + + @POST + @Path("/{service}/invalidate-cache-all") + public void invalidateCacheAllFromService(@PathParam("service") String service) { + lookupServiceByPathParam(service).invalidateAll(); + } + + @GET + @Path("/{service}/using-prefix/{prefix}") + @Produces(MediaType.TEXT_PLAIN) + public String getValueUsingPrefixFromService(@PathParam("service") String service, @PathParam("prefix") String prefix) { + return lookupServiceByPathParam(service).getValueWithPrefix(prefix); + } + + @POST + @Path("/{service}/using-prefix/{prefix}/invalidate-cache") + public void invalidateCacheUsingPrefixFromService(@PathParam("service") String service, + @PathParam("prefix") String prefix) { + lookupServiceByPathParam(service).invalidateWithPrefix(prefix); + } + + private BaseServiceWithCache lookupServiceByPathParam(String service) { + if (APPLICATION_SCOPE_SERVICE_PATH.equals(service)) { + return applicationScopeService; + } else if (REQUEST_SCOPE_SERVICE_PATH.equals(service)) { + return requestScopeService; + } + + throw new IllegalArgumentException("Service " + service + " is not recognised"); + } +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java new file mode 100644 index 000000000..0e2bb2a76 --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java @@ -0,0 +1,31 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.qute.Template; + +@Path("template") +public class TemplateCacheResource { + + @Inject + Template cached; + + /** + * Check for remote cache with Qute template. Qute should not use remote cache. + * See https://github.com/quarkusio/quarkus/issues/35680#issuecomment-1711153725 + * + * @return Should return error contains `not supported for remote caches` + */ + @GET + @Path("error") + public String getQuteTemplate() { + try { + return cached.render(); + } catch (IllegalStateException e) { + return e.getMessage(); + } + } + +} diff --git a/cache/redis/src/main/resources/application.properties b/cache/redis/src/main/resources/application.properties new file mode 100644 index 000000000..e69de29bb diff --git a/cache/redis/src/main/resources/templates/cached.html b/cache/redis/src/main/resources/templates/cached.html new file mode 100644 index 000000000..55584772d --- /dev/null +++ b/cache/redis/src/main/resources/templates/cached.html @@ -0,0 +1 @@ +{#cached}This cached template won't be working with remote cache like redis.{/cached} diff --git a/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java new file mode 100644 index 000000000..e96cf029d --- /dev/null +++ b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.redis; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftRedisCacheIT extends RedisCacheIT { +} diff --git a/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java new file mode 100644 index 000000000..75fa38fe9 --- /dev/null +++ b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java @@ -0,0 +1,185 @@ +package io.quarkus.ts.cache.redis; + +import static io.quarkus.ts.cache.caffeine.ServiceWithCacheResource.APPLICATION_SCOPE_SERVICE_PATH; +import static io.quarkus.ts.cache.caffeine.ServiceWithCacheResource.REQUEST_SCOPE_SERVICE_PATH; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.test.bootstrap.DefaultService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class RedisCacheIT { + + private static final String SERVICE_APPLICATION_SCOPE_PATH = "/services/" + APPLICATION_SCOPE_SERVICE_PATH; + private static final String SERVICE_REQUEST_SCOPE_PATH = "/services/" + REQUEST_SCOPE_SERVICE_PATH; + + private static final String PREFIX_ONE = "prefix1"; + private static final String PREFIX_TWO = "prefix2"; + + private static final int REDIS_PORT = 6379; + + @Container(image = "${redis.image}", port = REDIS_PORT, expectedLog = "Ready to accept connections") + static DefaultService redis = new DefaultService().withProperty("ALLOW_EMPTY_PASSWORD", "YES"); + + @QuarkusApplication + static RestService app = new RestService() + .withProperty("quarkus.redis.hosts", + () -> { + String redisHost = redis.getURI().withScheme("redis").getRestAssuredStyleUri(); + return String.format("%s:%d", redisHost, redis.getURI().getPort()); + }); + + /** + * Check whether the `@CacheResult` annotation works when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetTheSameValueAlwaysWhenGettingValueFromPath(String path) { + // We call the service endpoint + String value = getFromPath(path); + + // At this point, the cache is populated and we should get the same value from the cache + assertEquals(value, getFromPath(path), "Value was different which means cache is not working"); + } + + /** + * Check whether the `@CacheInvalidate` annotation invalidates the cache when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetDifferentValueWhenInvalidateCacheFromPath(String path) { + // We call the service endpoint + String value = getFromPath(path); + + // invalidate the cache + invalidateCacheFromPath(path); + + // Then the value should be different as we have invalidated the cache. + assertNotEquals(value, getFromPath(path), "Value was equal which means cache invalidate didn't work"); + } + + /** + * Check whether the `@CacheResult` annotation works when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetTheSameValueForSamePrefixesWhenGettingValueFromPath(String path) { + // We call the service endpoint + String value = getValueFromPathUsingPrefix(path, PREFIX_ONE); + + // At this point, the cache is populated and we should get the same value from the cache + assertEquals(value, getValueFromPathUsingPrefix(path, PREFIX_ONE), + "Value was different which means cache is not working"); + // But different value using another prefix + assertNotEquals(value, getValueFromPathUsingPrefix(path, PREFIX_TWO), + "Value was equal which means @CacheKey didn't work"); + } + + /** + * Check whether the `@CacheInvalidate` annotation does not invalidate all the caches + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetTheSameValuesEvenAfterCallingToCacheInvalidateFromPath(String path) { + // We call the service endpoints + String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE); + String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO); + + // invalidate the cache: this should not invalidate all the keys + invalidateCacheFromPath(path); + + // At this point, the cache is populated and we should get the same value for both prefixes + assertEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE)); + assertEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO)); + } + + /** + * Check whether the `@CacheInvalidate` and `@CacheKey` annotations work as expected. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetDifferentValueWhenInvalidateCacheOnlyForOnePrefixFromPath(String path) { + // We call the service endpoints + String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE); + String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO); + + // invalidate the cache: this should not invalidate all the keys + invalidateCacheWithPrefixFromPath(path, PREFIX_ONE); + + // The cache was invalidated only for prefix1, so the value should be different + assertNotEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE)); + // The cache was not invalidated for prefix2, so the value should be the same + assertEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO)); + } + + /** + * Check whether the `@CacheInvalidateAll` annotation works as expected. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetDifferentValueWhenInvalidateAllTheCacheFromPath(String path) { + // We call the service endpoints + String value = getFromPath(path); + String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE); + String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO); + + // invalidate all the cache + invalidateCacheAllFromPath(path); + + // Then, all the values should be different: + assertNotEquals(value, getFromPath(path)); + assertNotEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE)); + assertNotEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO)); + } + + /** + * Check if the usage of Qute and redis throw expected error + */ + @Test + public void quteShouldThrowError() { + assertThat(getFromPath("/template/error"), containsString("not supported for remote caches")); + } + + private void invalidateCacheAllFromPath(String path) { + postFromPath(path + "/invalidate-cache-all"); + } + + private void invalidateCacheWithPrefixFromPath(String path, String prefix) { + postFromPath(path + "/using-prefix/" + prefix + "/invalidate-cache"); + } + + private void invalidateCacheFromPath(String path) { + postFromPath(path + "/invalidate-cache"); + } + + private String getValueFromPathUsingPrefix(String path, String prefix) { + return getFromPath(path + "/using-prefix/" + prefix); + } + + private String getFromPath(String path) { + return app.given() + .when().get(path) + .then() + .statusCode(HttpStatus.SC_OK) + .extract().asString(); + } + + private void postFromPath(String path) { + app.given() + .when().post(path) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + +} diff --git a/cache/redis/src/test/resources/test.properties b/cache/redis/src/test/resources/test.properties new file mode 100644 index 000000000..a72083e30 --- /dev/null +++ b/cache/redis/src/test/resources/test.properties @@ -0,0 +1 @@ +ts.redis.openshift.use-internal-service-as-url=true diff --git a/pom.xml b/pom.xml index 6fb9fde66..015e8769f 100644 --- a/pom.xml +++ b/pom.xml @@ -451,6 +451,7 @@ env-info cache/caffeine + cache/redis infinispan-client From 7c0871b2db3b1907113a647669013eb8fb891bb0 Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Thu, 16 Nov 2023 20:28:20 +0100 Subject: [PATCH 2/2] Update redis version from 6.0 to 7.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 015e8769f..0cfc18c7c 100644 --- a/pom.xml +++ b/pom.xml @@ -264,7 +264,7 @@ docker.io/gvenzl/oracle-free:23-slim-faststart quay.io/quarkusqeteam/db2:11.5.7.0 docker.io/library/mongo:5.0 - docker.io/library/redis:6.0 + docker.io/library/redis:7.2 quay.io/ocpmetal/wiremock docker.io/bitnami/consul:1.15.2