diff --git a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ApplicationScopeService.java b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ApplicationScopeService.java
index baf9fbebb3..04076d2c7e 100644
--- a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ApplicationScopeService.java
+++ b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ApplicationScopeService.java
@@ -1,4 +1,4 @@
-package io.quarkus.ts.cache.caffeine;
+package io.quarkus.ts.cache.caffeine.cache.caffeine;
import jakarta.enterprise.context.ApplicationScoped;
diff --git a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BaseServiceWithCache.java b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BaseServiceWithCache.java
index c93a2e55d1..8117f1727c 100644
--- a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BaseServiceWithCache.java
+++ b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BaseServiceWithCache.java
@@ -1,4 +1,4 @@
-package io.quarkus.ts.cache.caffeine;
+package io.quarkus.ts.cache.caffeine.cache.caffeine;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
diff --git a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BlockingWithCacheResource.java b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BlockingWithCacheResource.java
index c8f56a7b99..77ec1a3d02 100644
--- a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BlockingWithCacheResource.java
+++ b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/BlockingWithCacheResource.java
@@ -1,4 +1,4 @@
-package io.quarkus.ts.cache.caffeine;
+package io.quarkus.ts.cache.caffeine.cache.caffeine;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
diff --git a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ReactiveWithCacheResource.java b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ReactiveWithCacheResource.java
index 76bb2c9094..386005afb1 100644
--- a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ReactiveWithCacheResource.java
+++ b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ReactiveWithCacheResource.java
@@ -1,4 +1,4 @@
-package io.quarkus.ts.cache.caffeine;
+package io.quarkus.ts.cache.caffeine.cache.caffeine;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
diff --git a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/RequestScopeService.java b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/RequestScopeService.java
index 8e12392b55..f3cd82c569 100644
--- a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/RequestScopeService.java
+++ b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/RequestScopeService.java
@@ -1,4 +1,4 @@
-package io.quarkus.ts.cache.caffeine;
+package io.quarkus.ts.cache.caffeine.cache.caffeine;
import jakarta.enterprise.context.RequestScoped;
diff --git a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ServiceWithCacheResource.java b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ServiceWithCacheResource.java
index e71870276a..5d39d87cfb 100644
--- a/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ServiceWithCacheResource.java
+++ b/cache/caffeine/src/main/java/io/quarkus/ts/cache/caffeine/ServiceWithCacheResource.java
@@ -1,4 +1,4 @@
-package io.quarkus.ts.cache.caffeine;
+package io.quarkus.ts.cache.caffeine.cache.caffeine;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
diff --git a/cache/caffeine/src/test/java/io/quarkus/ts/cache/caffeine/cache/caffeine/CaffeineCacheIT.java b/cache/caffeine/src/test/java/io/quarkus/ts/cache/caffeine/cache/caffeine/CaffeineCacheIT.java
index 33b9b368dc..e318300d78 100644
--- a/cache/caffeine/src/test/java/io/quarkus/ts/cache/caffeine/cache/caffeine/CaffeineCacheIT.java
+++ b/cache/caffeine/src/test/java/io/quarkus/ts/cache/caffeine/cache/caffeine/CaffeineCacheIT.java
@@ -1,7 +1,7 @@
package io.quarkus.ts.cache.caffeine.cache.caffeine;
-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 io.quarkus.ts.cache.caffeine.cache.caffeine.ServiceWithCacheResource.APPLICATION_SCOPE_SERVICE_PATH;
+import static io.quarkus.ts.cache.caffeine.cache.caffeine.ServiceWithCacheResource.REQUEST_SCOPE_SERVICE_PATH;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
diff --git a/cache/redis/pom.xml b/cache/redis/pom.xml
new file mode 100644
index 0000000000..51f1e140ea
--- /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 0000000000..baf9fbebb3
--- /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 0000000000..c93a2e55d1
--- /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 0000000000..8e12392b55
--- /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 0000000000..e71870276a
--- /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 0000000000..0e2bb2a76c
--- /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 0000000000..e69de29bb2
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 0000000000..55584772d0
--- /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 0000000000..e96cf029d8
--- /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 0000000000..75fa38fe97
--- /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 0000000000..a72083e303
--- /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 bde6cded91..fa5a94bc29 100644
--- a/pom.xml
+++ b/pom.xml
@@ -415,6 +415,7 @@
quarkus-cli
logging/jboss
cache/caffeine
+ cache/redis
qute/multimodule
qute/synchronous
qute/reactive