diff --git a/.github/native-tests.json b/.github/native-tests.json
index 451fb30e2cd61..7aede39fdb855 100644
--- a/.github/native-tests.json
+++ b/.github/native-tests.json
@@ -86,8 +86,8 @@
},
{
"category": "Cache",
- "timeout": 55,
- "test-modules": "infinispan-cache-jpa, infinispan-client, cache",
+ "timeout": 60,
+ "test-modules": "infinispan-cache-jpa, infinispan-client, cache, redis-cache",
"os-name": "ubuntu-latest"
},
{
diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index d81fa3effa617..d24a90f83ac17 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -2808,6 +2808,11 @@
quarkus-cache-deployment-spi${project.version}
+
+ io.quarkus
+ quarkus-cache-runtime-spi
+ ${project.version}
+ io.quarkusquarkus-google-cloud-functions
@@ -5856,12 +5861,22 @@
quarkus-redis-client${project.version}
+
+ io.quarkus
+ quarkus-redis-cache
+ ${project.version}
+ io.quarkusquarkus-redis-client-deployment${project.version}
+
+ io.quarkus
+ quarkus-redis-cache-deployment
+ ${project.version}
+
diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml
index 541ac67e002f5..bd040b5384fc6 100644
--- a/devtools/bom-descriptor-json/pom.xml
+++ b/devtools/bom-descriptor-json/pom.xml
@@ -1786,6 +1786,19 @@
+
+ io.quarkus
+ quarkus-redis-cache
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+ io.quarkusquarkus-redis-client
diff --git a/docs/pom.xml b/docs/pom.xml
index 2d7f10d67f1dc..38a5d15e6b12e 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -1796,6 +1796,19 @@
+
+ io.quarkus
+ quarkus-redis-cache-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+ io.quarkusquarkus-redis-client-deployment
diff --git a/docs/src/main/asciidoc/cache-redis-reference.adoc b/docs/src/main/asciidoc/cache-redis-reference.adoc
new file mode 100644
index 0000000000000..cec715482b675
--- /dev/null
+++ b/docs/src/main/asciidoc/cache-redis-reference.adoc
@@ -0,0 +1,151 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
+////
+= Redis Cache
+:extension-status: preview
+include::_attributes.adoc[]
+:categories: data
+:summary: Use Redis as the Quarkus cache backend
+
+By default, Quarkus Cache uses Caffeine as backend.
+It's possible to use Redis instead.
+
+include::{includes}/extension-status.adoc[]
+
+== Redis as cache backend
+
+When using Redis as the backend for Quarkus cache, each cached item will be stored in Redis:
+
+- The backend uses the __ Redis client (if not configured otherwise), so make sure it's configured (or use the xref:redis-dev-services.adoc[redis dev service])
+- the Redis key is built as follows: `cache:$cache-name:$cache-key`, where `cache-key` is the key the application uses.
+- the value is encoded to JSON if needed
+
+
+== Use the Redis backend
+
+First, you need to add the `quarkus-redis-cache` extension to your project:
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+ io.quarkus
+ quarkus-redis-cache
+
+----
+
+[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
+.build.gradle
+----
+implementation("io.quarkus:quarkus-redis-cache")
+----
+
+Then, use the `@CacheResult` and others cache annotations as explained in the xref:cache.adoc[Quarkus Cache guide]:
+
+[source, java]
+----
+@GET
+@Path("/{keyElement1}/{keyElement2}/{keyElement3}")
+@CacheResult(cacheName = "expensiveResourceCache")
+public ExpensiveResponse getExpensiveResponse(@PathParam("keyElement1") @CacheKey String keyElement1,
+ @PathParam("keyElement2") @CacheKey String keyElement2, @PathParam("keyElement3") @CacheKey String keyElement3,
+ @QueryParam("foo") String foo) {
+ invocations.incrementAndGet();
+ ExpensiveResponse response = new ExpensiveResponse();
+ response.setResult(keyElement1 + " " + keyElement2 + " " + keyElement3 + " too!");
+ return response;
+}
+
+@POST
+@CacheInvalidateAll(cacheName = "expensiveResourceCache")
+public void invalidateAll() {
+
+}
+----
+
+[[redis-cache-configuration-reference]]
+== Configure the Redis backend
+
+The Redis backend uses the `` Redis client.
+See the xref:redis-reference.adoc[Redis reference] to configure the access to Redis.
+
+TIP: In dev mode, you can use the xref:redis-dev-services.adoc[Redis Dev Service].
+
+If you want to use another Redis for your cache, configure the `client-name` as follows:
+
+[source, properties]
+----
+quarkus.cache.redis.client-name=my-redis-for-cache
+----
+
+When writing to Redis or reading from Redis, Quarkus needs to know the type.
+Indeed, the objects need to be serialized and deserialized.
+For that purpose, you may need to configure type (class names) of the key and value you want to cache.
+At build time, Quarkus tries to deduce the types from the application code, but that decision can be overridden using:
+
+[source, properties]
+----
+# Default configuration
+quarkus.cache.redis.key-type=java.lang.String
+quarkus.cache.redis.value-type=org.acme.Person
+
+# Configuration for `expensiveResourceCache`
+quarkus.cache.redis.expensiveResourceCache.key-type=java.lang.String
+quarkus.cache.redis.expensiveResourceCache.value-type=org.acme.Supes
+----
+
+You can also configure the time to live of the cached entries:
+
+[source, properties]
+----
+# Default configuration
+quarkus.cache.redis.ttl=10s
+
+# Configuration for `expensiveResourceCache`
+quarkus.cache.redis.expensiveResourceCache.ttl=1h
+----
+
+If the `ttl` is not configured, the entry won't be evicted.
+You would need to invalidate the values using the `@CacheInvalidateAll` or `@CacheInvalidate` annotations.
+
+The following table lists the supported properties:
+
+include::{generated-dir}/config/quarkus-cache-redis.adoc[opts=optional, leveloffset=+1]
+
+== Configure the Redis key
+
+By default, the Redis backend stores the entry using the following keys: `cache:$cache-name:$cache-key`, where `cache-key` is the key the application uses.
+So, you can find all the entries for a single cache using the Redis `KEYS` command: `KEYS cache:$cache-name:*`
+
+The `cache:$cache-name:` segment can be configured using the `prefix` property:
+
+
+[source, properties]
+----
+# Default configuration
+quarkus.cache.redis.prefix=my-cache
+
+# Configuration for `expensiveResourceCache`
+quarkus.cache.redis.expensiveResourceCache.prefix=my-expensive-cache
+----
+
+In these cases, you can find all the keys managed by the default cache using `KEYS my-cache:*`, and all the keys managed by the `expensiveResourceCache` cache using: `KEYS my-expensive-cache:*`.
+
+== Enable optimistic locking
+
+The access to the cache can be _direct_ or use https://redis.io/docs/manual/transactions/#optimistic-locking-using-check-and-set[optimistic locking].
+By default, optimistic locking is disabled.
+
+You can enable optimistic locking using:
+[source, properties]
+----
+# Default configuration
+quarkus.cache.redis.use-optimistic-locking=true
+
+# Configuration for `expensiveResourceCache`
+quarkus.cache.redis.expensiveResourceCache.use-optimistic-locking=true
+----
+
+When used, the key is _watched_ and the _SET_ command is executed in a transaction (`MULTI/EXEC`).
diff --git a/docs/src/main/asciidoc/cache.adoc b/docs/src/main/asciidoc/cache.adoc
index 006cd16d9f268..aa8fbc85f2cbf 100644
--- a/docs/src/main/asciidoc/cache.adoc
+++ b/docs/src/main/asciidoc/cache.adoc
@@ -25,6 +25,13 @@ Since the weather forecast is updated once every twelve hours, caching the servi
We'll do that using a single Quarkus annotation.
+[NOTE]
+====
+In this guide, we use the default Quarkus Cache backend (Caffeine).
+You can use Redis instead.
+Refer to the xref:cache-redis-reference.adoc[Redis cache backend reference] to configure the Redis backend.
+====
+
== Solution
We recommend that you follow the instructions in the next sections and create the application step by step.
@@ -736,7 +743,7 @@ properties in the `application.properties` file. By default, caches do not perfo
You need to replace `cache-name` in all the following properties with the real name of the cache you want to configure.
====
-include::{generated-dir}/config/quarkus-cache-config-group-cache-config-caffeine-config.adoc[opts=optional, leveloffset=+1]
+include::{generated-dir}/config/quarkus-cache-cache-config.adoc[opts=optional, leveloffset=+1]
Here's what your cache configuration could look like:
diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc
index 83c29b9f20219..a9e3d1e882229 100644
--- a/docs/src/main/asciidoc/redis-reference.adoc
+++ b/docs/src/main/asciidoc/redis-reference.adoc
@@ -55,6 +55,9 @@ import io.quarkus.redis.datasource.RedisDataSource;
More details about the various APIs offered by the quarkus-redis extension are available in the <> section.
+[NOTE]
+To use Redis as a cache backend, refer to the xref:cache-redis-reference.adoc[Redis Cache Backend reference].
+
[[apis]]
== One extension, multiple APIs
diff --git a/extensions/cache/deployment-spi/pom.xml b/extensions/cache/deployment-spi/pom.xml
index 1cfd1d01e99c2..9e5bda717185f 100644
--- a/extensions/cache/deployment-spi/pom.xml
+++ b/extensions/cache/deployment-spi/pom.xml
@@ -18,6 +18,10 @@
io.quarkusquarkus-core-deployment
+
+ io.quarkus
+ quarkus-cache-runtime-spi
+
diff --git a/extensions/cache/deployment-spi/src/main/java/io/quarkus/cache/deployment/spi/CacheManagerInfoBuildItem.java b/extensions/cache/deployment-spi/src/main/java/io/quarkus/cache/deployment/spi/CacheManagerInfoBuildItem.java
new file mode 100644
index 0000000000000..08863e998b912
--- /dev/null
+++ b/extensions/cache/deployment-spi/src/main/java/io/quarkus/cache/deployment/spi/CacheManagerInfoBuildItem.java
@@ -0,0 +1,20 @@
+package io.quarkus.cache.deployment.spi;
+
+import io.quarkus.builder.item.MultiBuildItem;
+import io.quarkus.cache.CacheManagerInfo;
+
+/**
+ * A build item that makes sure a {@link CacheManagerInfo} is available at runtime for consideration as the cache backend
+ */
+public final class CacheManagerInfoBuildItem extends MultiBuildItem {
+
+ private final CacheManagerInfo info;
+
+ public CacheManagerInfoBuildItem(CacheManagerInfo info) {
+ this.info = info;
+ }
+
+ public CacheManagerInfo get() {
+ return info;
+ }
+}
diff --git a/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java b/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java
index ed97db3e07d74..020f15d8b5130 100644
--- a/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java
+++ b/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java
@@ -53,6 +53,7 @@
import io.quarkus.cache.deployment.exception.UnsupportedRepeatedAnnotationException;
import io.quarkus.cache.deployment.exception.VoidReturnTypeTargetException;
import io.quarkus.cache.deployment.spi.AdditionalCacheNameBuildItem;
+import io.quarkus.cache.deployment.spi.CacheManagerInfoBuildItem;
import io.quarkus.cache.runtime.CacheInvalidateAllInterceptor;
import io.quarkus.cache.runtime.CacheInvalidateInterceptor;
import io.quarkus.cache.runtime.CacheManagerRecorder;
@@ -238,17 +239,26 @@ private List validateKeyGeneratorsDefaultConstructor(CombinedIndexBui
@BuildStep
@Record(RUNTIME_INIT)
- SyntheticBeanBuildItem configureCacheManagerSyntheticBean(CacheNamesBuildItem cacheNames,
- CacheManagerRecorder cacheManagerRecorder, Optional metricsCapability) {
+ void cacheManagerInfos(BuildProducer producer,
+ Optional metricsCapability, CacheManagerRecorder recorder) {
+ producer.produce(new CacheManagerInfoBuildItem(recorder.noOpCacheManagerInfo()));
+ producer.produce(new CacheManagerInfoBuildItem(recorder.getCacheManagerInfoWithoutMetrics()));
+ if (metricsCapability.isPresent() && metricsCapability.get().metricsSupported(MICROMETER)) {
+ // if we include this unconditionally the native image building will fail when Micrometer is not around
+ producer.produce(new CacheManagerInfoBuildItem(recorder.getCacheManagerInfoWithMicrometerMetrics()));
+ }
+ }
- boolean micrometerSupported = metricsCapability.isPresent() && metricsCapability.get().metricsSupported(MICROMETER);
+ @BuildStep
+ @Record(RUNTIME_INIT)
+ SyntheticBeanBuildItem configureCacheManagerSyntheticBean(List infos,
+ CacheNamesBuildItem cacheNames, Optional metricsCapability,
+ CacheManagerRecorder cacheManagerRecorder) {
- Supplier cacheManagerSupplier;
- if (micrometerSupported) {
- cacheManagerSupplier = cacheManagerRecorder.getCacheManagerSupplierWithMicrometerMetrics(cacheNames.getNames());
- } else {
- cacheManagerSupplier = cacheManagerRecorder.getCacheManagerSupplierWithoutMetrics(cacheNames.getNames());
- }
+ boolean micrometerSupported = metricsCapability.isPresent() && metricsCapability.get().metricsSupported(MICROMETER);
+ Supplier cacheManagerSupplier = cacheManagerRecorder.resolveCacheInfo(
+ infos.stream().map(CacheManagerInfoBuildItem::get).collect(toList()), cacheNames.getNames(),
+ micrometerSupported);
return SyntheticBeanBuildItem.configure(CacheManager.class)
.scope(ApplicationScoped.class)
diff --git a/extensions/cache/pom.xml b/extensions/cache/pom.xml
index 092198414e79c..a7f580a571601 100644
--- a/extensions/cache/pom.xml
+++ b/extensions/cache/pom.xml
@@ -19,5 +19,6 @@
deploymentdeployment-spiruntime
+ runtime-spi
diff --git a/extensions/cache/runtime-spi/pom.xml b/extensions/cache/runtime-spi/pom.xml
new file mode 100644
index 0000000000000..19619b5e24a5f
--- /dev/null
+++ b/extensions/cache/runtime-spi/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ io.quarkus
+ quarkus-cache-parent
+ 999-SNAPSHOT
+
+ 4.0.0
+
+ quarkus-cache-runtime-spi
+ Quarkus - Cache - Runtime SPI
+
+
+
+ io.smallrye.reactive
+ mutiny
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
+
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/Cache.java b/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/Cache.java
similarity index 89%
rename from extensions/cache/runtime/src/main/java/io/quarkus/cache/Cache.java
rename to extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/Cache.java
index a7d7b3e9b2637..baba8744869a7 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/Cache.java
+++ b/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/Cache.java
@@ -7,7 +7,7 @@
/**
* Use this interface to interact with a cache programmatically e.g. store, retrieve or delete cache values. The cache can be
- * injected using the {@link CacheName} annotation or retrieved using {@link CacheManager#getCache(String)}.
+ * injected using the {@code @io.quarkus.cache.CacheName} annotation or retrieved using {@link CacheManager#getCache(String)}.
*/
public interface Cache {
@@ -20,7 +20,8 @@ public interface Cache {
/**
* Returns the unique and immutable default key for the current cache. This key is used by the annotations caching API when
- * a no-args method annotated with {@link CacheResult} or {@link CacheInvalidate} is invoked. It can also be used with the
+ * a no-args method annotated with {@code @io.quarkus.cache.CacheResult} or {@code @io.quarkus.cache.CacheInvalidate} is
+ * invoked. It can also be used with the
* programmatic caching API.
*
* @return default cache key
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/CacheManager.java b/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/CacheManager.java
similarity index 91%
rename from extensions/cache/runtime/src/main/java/io/quarkus/cache/CacheManager.java
rename to extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/CacheManager.java
index ffedd8ad288e1..c62628ca1e089 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/CacheManager.java
+++ b/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/CacheManager.java
@@ -7,7 +7,7 @@
*
* Use this interface to retrieve all existing {@link Cache} names and interact with any cache programmatically e.g. store,
* retrieve or delete cache values. It shares the same caches collection the Quarkus caching annotations use. The
- * {@link CacheName} annotation can also be used to inject and access a specific cache from its name.
+ * {@code @io.quarkus.cache.CacheName} annotation can also be used to inject and access a specific cache from its name.
*
*
* Code example:
diff --git a/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/CacheManagerInfo.java b/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/CacheManagerInfo.java
new file mode 100644
index 0000000000000..cd46de0549184
--- /dev/null
+++ b/extensions/cache/runtime-spi/src/main/java/io/quarkus/cache/CacheManagerInfo.java
@@ -0,0 +1,27 @@
+package io.quarkus.cache;
+
+import java.util.Set;
+import java.util.function.Supplier;
+
+public interface CacheManagerInfo {
+
+ boolean supports(Context context);
+
+ Supplier get(Context context);
+
+ interface Context {
+
+ boolean cacheEnabled();
+
+ Metrics metrics();
+
+ String cacheType();
+
+ Set cacheNames();
+
+ enum Metrics {
+ NONE,
+ MICROMETER
+ }
+ }
+}
diff --git a/extensions/cache/runtime/pom.xml b/extensions/cache/runtime/pom.xml
index c87ec82e4e0ef..70993637b709a 100644
--- a/extensions/cache/runtime/pom.xml
+++ b/extensions/cache/runtime/pom.xml
@@ -27,6 +27,10 @@
io.quarkusquarkus-mutiny
+
+ io.quarkus
+ quarkus-cache-runtime-spi
+ io.vertxvertx-web
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/CompositeCacheKey.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/CompositeCacheKey.java
index 2803b832e5cc7..d06929756ac3a 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/CompositeCacheKey.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/CompositeCacheKey.java
@@ -46,4 +46,8 @@ public boolean equals(Object obj) {
public String toString() {
return "CompositeCacheKey" + Arrays.toString(keyElements);
}
+
+ public Object[] getKeyElements() {
+ return keyElements;
+ }
}
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheBuildConfig.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheBuildConfig.java
new file mode 100644
index 0000000000000..be01933b30a96
--- /dev/null
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheBuildConfig.java
@@ -0,0 +1,20 @@
+package io.quarkus.cache.runtime;
+
+import static io.quarkus.runtime.annotations.ConfigPhase.BUILD_AND_RUN_TIME_FIXED;
+
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigRoot(phase = BUILD_AND_RUN_TIME_FIXED)
+@ConfigMapping(prefix = "quarkus.cache")
+public interface CacheBuildConfig {
+
+ String CAFFEINE_CACHE_TYPE = "caffeine";
+
+ /**
+ * Cache type.
+ */
+ @WithDefault(CAFFEINE_CACHE_TYPE)
+ String type();
+}
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheConfig.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheConfig.java
index 56d46a8870da1..0ac4f0bad8331 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheConfig.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheConfig.java
@@ -10,60 +10,50 @@
import io.quarkus.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigDocSection;
-import io.quarkus.runtime.annotations.ConfigGroup;
-import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithParentName;
@ConfigRoot(phase = RUN_TIME)
-public class CacheConfig {
-
- public static final String CAFFEINE_CACHE_TYPE = "caffeine";
+@ConfigMapping(prefix = "quarkus.cache")
+public interface CacheConfig {
/**
* Whether or not the cache extension is enabled.
*/
- @ConfigItem(defaultValue = "true")
- public boolean enabled;
-
- /**
- * Cache type.
- */
- @ConfigItem(defaultValue = CAFFEINE_CACHE_TYPE)
- public String type;
+ @WithDefault("true")
+ boolean enabled();
/**
* Caffeine configuration.
*/
- @ConfigItem
- public CaffeineConfig caffeine;
+ CaffeineConfig caffeine();
- @ConfigGroup
- public static class CaffeineConfig {
+ interface CaffeineConfig {
/**
* Default configuration applied to all Caffeine caches (lowest precedence)
*/
- @ConfigItem(name = ConfigItem.PARENT)
+ @WithParentName
@ConfigDocSection
- public CaffeineCacheConfig defaultConfig;
+ CaffeineCacheConfig defaultConfig();
/**
* Additional configuration applied to a specific Caffeine cache (highest precedence)
*/
- @ConfigItem(name = ConfigItem.PARENT)
+ @WithParentName
@ConfigDocMapKey("cache-name")
@ConfigDocSection
- public Map cachesConfig;
+ Map cachesConfig();
- @ConfigGroup
- public static class CaffeineCacheConfig {
+ interface CaffeineCacheConfig {
/**
* Minimum total size for the internal data structures. Providing a large enough estimate at construction time
* avoids the need for expensive resizing operations later, but setting this value unnecessarily high wastes memory.
*/
- @ConfigItem
- public OptionalInt initialCapacity;
+ OptionalInt initialCapacity();
/**
* Maximum number of entries the cache may contain. Note that the cache may evict an entry before this limit is
@@ -71,29 +61,25 @@ public static class CaffeineCacheConfig {
* the cache evicts entries that are less likely to be used again. For example, the cache may evict an entry because
* it hasn't been used recently or very often.
*/
- @ConfigItem
- public OptionalLong maximumSize;
+ OptionalLong maximumSize();
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after
* the entry's creation, or the most recent replacement of its value.
*/
- @ConfigItem
- public Optional expireAfterWrite;
+ Optional expireAfterWrite();
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after
* the entry's creation, the most recent replacement of its value, or its last read.
*/
- @ConfigItem
- public Optional expireAfterAccess;
+ Optional expireAfterAccess();
/**
* Whether or not metrics are recorded if the application depends on the Micrometer extension. Setting this
* value to {@code true} will enable the accumulation of cache stats inside Caffeine.
*/
- @ConfigItem
- public Optional metricsEnabled;
+ Optional metricsEnabled();
}
}
}
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheManagerRecorder.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheManagerRecorder.java
index 8350ca55c6b28..2e2621edc114f 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheManagerRecorder.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheManagerRecorder.java
@@ -1,57 +1,105 @@
package io.quarkus.cache.runtime;
-import static io.quarkus.cache.runtime.CacheConfig.CAFFEINE_CACHE_TYPE;
+import static io.quarkus.cache.runtime.CacheBuildConfig.CAFFEINE_CACHE_TYPE;
+import java.util.Collection;
import java.util.Set;
import java.util.function.Supplier;
import jakarta.enterprise.inject.spi.DeploymentException;
import io.quarkus.cache.CacheManager;
+import io.quarkus.cache.CacheManagerInfo;
import io.quarkus.cache.runtime.caffeine.CaffeineCacheManagerBuilder;
import io.quarkus.cache.runtime.noop.NoOpCacheManagerBuilder;
+import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
@Recorder
public class CacheManagerRecorder {
- private final CacheConfig cacheConfig;
+ private final CacheBuildConfig cacheBuildConfig;
+ private final RuntimeValue cacheConfigRV;
- public CacheManagerRecorder(CacheConfig cacheConfig) {
- this.cacheConfig = cacheConfig;
+ public CacheManagerRecorder(CacheBuildConfig cacheBuildConfig, RuntimeValue cacheConfigRV) {
+ this.cacheBuildConfig = cacheBuildConfig;
+ this.cacheConfigRV = cacheConfigRV;
}
- public Supplier getCacheManagerSupplierWithMicrometerMetrics(Set cacheNames) {
- Supplier> caffeineCacheManagerSupplier = new Supplier>() {
+ public Supplier resolveCacheInfo(Collection infos, Set cacheNames,
+ boolean micrometerMetricsEnabled) {
+ CacheConfig cacheConfig = cacheConfigRV.getValue();
+ CacheManagerInfo.Context context = new CacheManagerInfo.Context() {
@Override
- public Supplier get() {
- return CaffeineCacheManagerBuilder.buildWithMicrometerMetrics(cacheNames, cacheConfig);
+ public boolean cacheEnabled() {
+ return cacheConfig.enabled();
+ }
+
+ @Override
+ public Metrics metrics() {
+ return micrometerMetricsEnabled ? Metrics.MICROMETER : Metrics.NONE;
+ }
+
+ @Override
+ public String cacheType() {
+ return cacheBuildConfig.type();
+ }
+
+ @Override
+ public Set cacheNames() {
+ return cacheNames;
}
};
- return getCacheManagerSupplier(cacheNames, caffeineCacheManagerSupplier);
+ for (CacheManagerInfo info : infos) {
+ if (info.supports(context)) {
+ return info.get(context);
+ }
+ }
+ throw new DeploymentException("Unknown cache type: " + context.cacheType());
}
- public Supplier getCacheManagerSupplierWithoutMetrics(Set cacheNames) {
- Supplier> caffeineCacheManagerSupplier = new Supplier>() {
+ public CacheManagerInfo noOpCacheManagerInfo() {
+ return new CacheManagerInfo() {
@Override
- public Supplier get() {
- return CaffeineCacheManagerBuilder.buildWithoutMetrics(cacheNames, cacheConfig);
+ public boolean supports(Context context) {
+ return !context.cacheEnabled();
+ }
+
+ @Override
+ public Supplier get(Context context) {
+ return NoOpCacheManagerBuilder.build(context.cacheNames());
}
};
- return getCacheManagerSupplier(cacheNames, caffeineCacheManagerSupplier);
}
- private Supplier getCacheManagerSupplier(Set cacheNames,
- Supplier> caffeineCacheManagerSupplier) {
- if (cacheConfig.enabled) {
- switch (cacheConfig.type) {
- case CAFFEINE_CACHE_TYPE:
- return caffeineCacheManagerSupplier.get();
- default:
- throw new DeploymentException("Unknown cache type: " + cacheConfig.type);
- }
- } else {
- return NoOpCacheManagerBuilder.build(cacheNames);
- }
+ public CacheManagerInfo getCacheManagerInfoWithMicrometerMetrics() {
+ return new CacheManagerInfo() {
+ @Override
+ public boolean supports(Context context) {
+ return context.cacheEnabled() && context.cacheType().equals(CAFFEINE_CACHE_TYPE)
+ && (context.metrics() == Context.Metrics.MICROMETER);
+ }
+
+ @Override
+ public Supplier get(Context context) {
+ return CaffeineCacheManagerBuilder.buildWithMicrometerMetrics(context.cacheNames(), cacheConfigRV.getValue());
+ }
+ };
}
+
+ public CacheManagerInfo getCacheManagerInfoWithoutMetrics() {
+ return new CacheManagerInfo() {
+ @Override
+ public boolean supports(Context context) {
+ return context.cacheEnabled() && context.cacheType().equals(CAFFEINE_CACHE_TYPE)
+ && (context.metrics() == Context.Metrics.NONE);
+ }
+
+ @Override
+ public Supplier get(Context context) {
+ return CaffeineCacheManagerBuilder.buildWithoutMetrics(context.cacheNames(), cacheConfigRV.getValue());
+ }
+ };
+ }
+
}
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java
index 80665cfcad6cd..c8baff5992b7a 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java
@@ -28,8 +28,9 @@
import io.smallrye.mutiny.Uni;
/**
- * This class is an internal Quarkus cache implementation. Do not use it explicitly from your Quarkus application. The public
- * methods signatures may change without prior notice.
+ * This class is an internal Quarkus cache implementation using Caffeine. Do not use it explicitly from your Quarkus
+ * application.
+ * The public methods signatures may change without prior notice.
*/
public class CaffeineCacheImpl extends AbstractCache implements CaffeineCache {
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheInfoBuilder.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheInfoBuilder.java
index e324300052acb..c5af6931e29d3 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheInfoBuilder.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheInfoBuilder.java
@@ -14,7 +14,7 @@ public static Set build(Set cacheNames, CacheConfig c
if (cacheNames.isEmpty()) {
return Collections.emptySet();
} else {
- CaffeineCacheConfig defaultConfig = cacheConfig.caffeine.defaultConfig;
+ CaffeineCacheConfig defaultConfig = cacheConfig.caffeine().defaultConfig();
Set cacheInfos = HashSetFactory. getInstance().apply(cacheNames.size());
for (String cacheName : cacheNames) {
@@ -22,36 +22,36 @@ public static Set build(Set cacheNames, CacheConfig c
CaffeineCacheInfo cacheInfo = new CaffeineCacheInfo();
cacheInfo.name = cacheName;
- CaffeineCacheConfig namedCacheConfig = cacheConfig.caffeine.cachesConfig.get(cacheInfo.name);
+ CaffeineCacheConfig namedCacheConfig = cacheConfig.caffeine().cachesConfig().get(cacheInfo.name);
- if (namedCacheConfig != null && namedCacheConfig.initialCapacity.isPresent()) {
- cacheInfo.initialCapacity = namedCacheConfig.initialCapacity.getAsInt();
- } else if (defaultConfig.initialCapacity.isPresent()) {
- cacheInfo.initialCapacity = defaultConfig.initialCapacity.getAsInt();
+ if (namedCacheConfig != null && namedCacheConfig.initialCapacity().isPresent()) {
+ cacheInfo.initialCapacity = namedCacheConfig.initialCapacity().getAsInt();
+ } else if (defaultConfig.initialCapacity().isPresent()) {
+ cacheInfo.initialCapacity = defaultConfig.initialCapacity().getAsInt();
}
- if (namedCacheConfig != null && namedCacheConfig.maximumSize.isPresent()) {
- cacheInfo.maximumSize = namedCacheConfig.maximumSize.getAsLong();
- } else if (defaultConfig.maximumSize.isPresent()) {
- cacheInfo.maximumSize = defaultConfig.maximumSize.getAsLong();
+ if (namedCacheConfig != null && namedCacheConfig.maximumSize().isPresent()) {
+ cacheInfo.maximumSize = namedCacheConfig.maximumSize().getAsLong();
+ } else if (defaultConfig.maximumSize().isPresent()) {
+ cacheInfo.maximumSize = defaultConfig.maximumSize().getAsLong();
}
- if (namedCacheConfig != null && namedCacheConfig.expireAfterWrite.isPresent()) {
- cacheInfo.expireAfterWrite = namedCacheConfig.expireAfterWrite.get();
- } else if (defaultConfig.expireAfterWrite.isPresent()) {
- cacheInfo.expireAfterWrite = defaultConfig.expireAfterWrite.get();
+ if (namedCacheConfig != null && namedCacheConfig.expireAfterWrite().isPresent()) {
+ cacheInfo.expireAfterWrite = namedCacheConfig.expireAfterWrite().get();
+ } else if (defaultConfig.expireAfterWrite().isPresent()) {
+ cacheInfo.expireAfterWrite = defaultConfig.expireAfterWrite().get();
}
- if (namedCacheConfig != null && namedCacheConfig.expireAfterAccess.isPresent()) {
- cacheInfo.expireAfterAccess = namedCacheConfig.expireAfterAccess.get();
- } else if (defaultConfig.expireAfterAccess.isPresent()) {
- cacheInfo.expireAfterAccess = defaultConfig.expireAfterAccess.get();
+ if (namedCacheConfig != null && namedCacheConfig.expireAfterAccess().isPresent()) {
+ cacheInfo.expireAfterAccess = namedCacheConfig.expireAfterAccess().get();
+ } else if (defaultConfig.expireAfterAccess().isPresent()) {
+ cacheInfo.expireAfterAccess = defaultConfig.expireAfterAccess().get();
}
- if (namedCacheConfig != null && namedCacheConfig.metricsEnabled.isPresent()) {
- cacheInfo.metricsEnabled = namedCacheConfig.metricsEnabled.get();
- } else if (defaultConfig.metricsEnabled.isPresent()) {
- cacheInfo.metricsEnabled = defaultConfig.metricsEnabled.get();
+ if (namedCacheConfig != null && namedCacheConfig.metricsEnabled().isPresent()) {
+ cacheInfo.metricsEnabled = namedCacheConfig.metricsEnabled().get();
+ } else if (defaultConfig.metricsEnabled().isPresent()) {
+ cacheInfo.metricsEnabled = defaultConfig.metricsEnabled().get();
}
cacheInfos.add(cacheInfo);
diff --git a/extensions/pom.xml b/extensions/pom.xml
index e443cd5dffe96..b2ea1473f0fa3 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -79,6 +79,7 @@
mailergrpcredis-client
+ redis-cachetransaction-annotations
diff --git a/extensions/redis-cache/deployment/pom.xml b/extensions/redis-cache/deployment/pom.xml
new file mode 100644
index 0000000000000..078f0184d1cf2
--- /dev/null
+++ b/extensions/redis-cache/deployment/pom.xml
@@ -0,0 +1,111 @@
+
+
+ 4.0.0
+
+
+ io.quarkus
+ quarkus-redis-cache-parent
+ 999-SNAPSHOT
+
+
+ quarkus-redis-cache-deployment
+
+ Quarkus - Redis Cache - Deployment
+
+
+
+ io.quarkus
+ quarkus-redis-client-deployment
+
+
+ io.quarkus
+ quarkus-cache-deployment
+
+
+ io.quarkus
+ quarkus-redis-cache
+
+
+
+ org.testcontainers
+ testcontainers
+
+
+ junit
+ junit
+
+
+
+
+ io.quarkus
+ quarkus-junit4-mock
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+
+ test-redis
+
+
+ test-containers
+
+
+
+
+
+ maven-surefire-plugin
+
+ false
+
+
+
+ maven-failsafe-plugin
+
+ false
+
+
+
+
+
+
+
diff --git a/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java b/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java
new file mode 100644
index 0000000000000..ebbce3ad96db5
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java
@@ -0,0 +1,179 @@
+package io.quarkus.cache.redis.deployment;
+
+import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
+import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
+import static org.jboss.jandex.AnnotationTarget.Kind.METHOD;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import jakarta.enterprise.inject.spi.DeploymentException;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.ParameterizedType;
+import org.jboss.jandex.Type;
+import org.jboss.logging.Logger;
+import org.jetbrains.annotations.NotNull;
+
+import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
+import io.quarkus.cache.CompositeCacheKey;
+import io.quarkus.cache.deployment.CacheDeploymentConstants;
+import io.quarkus.cache.deployment.CacheNamesBuildItem;
+import io.quarkus.cache.deployment.spi.CacheManagerInfoBuildItem;
+import io.quarkus.cache.redis.runtime.RedisCacheBuildRecorder;
+import io.quarkus.cache.redis.runtime.RedisCacheBuildTimeConfig;
+import io.quarkus.cache.redis.runtime.RedisCachesBuildTimeConfig;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.redis.client.deployment.RequestedRedisClientBuildItem;
+import io.quarkus.redis.runtime.client.config.RedisConfig;
+import io.smallrye.mutiny.Uni;
+
+public class RedisCacheProcessor {
+
+ private static final Logger LOGGER = Logger.getLogger(RedisCacheProcessor.class);
+
+ public static final DotName UNI = DotName.createSimple(Uni.class.getName());
+
+ @BuildStep
+ @Record(RUNTIME_INIT)
+ CacheManagerInfoBuildItem cacheManagerInfo(RedisCacheBuildRecorder recorder) {
+ return new CacheManagerInfoBuildItem(recorder.getCacheManagerSupplier());
+ }
+
+ @BuildStep
+ UnremovableBeanBuildItem redisClientUnremoveable() {
+ return UnremovableBeanBuildItem.beanTypes(io.vertx.redis.client.Redis.class, io.vertx.mutiny.redis.client.Redis.class);
+ }
+
+ @BuildStep
+ RequestedRedisClientBuildItem requestedRedisClientBuildItem(RedisCachesBuildTimeConfig buildConfig) {
+ return new RequestedRedisClientBuildItem(buildConfig.clientName.orElse(RedisConfig.DEFAULT_CLIENT_NAME));
+ }
+
+ @BuildStep
+ RunTimeConfigurationDefaultBuildItem redisCacheByDefault() {
+ return new RunTimeConfigurationDefaultBuildItem("quarkus.cache.type", "redis");
+ }
+
+ @BuildStep
+ void nativeImage(BuildProducer producer) {
+ producer.produce(ReflectiveClassBuildItem.builder(CompositeCacheKey.class).methods(true).build());
+ }
+
+ @BuildStep
+ @Record(STATIC_INIT)
+ void determineValueTypes(RedisCacheBuildRecorder recorder, CombinedIndexBuildItem combinedIndex,
+ CacheNamesBuildItem cacheNamesBuildItem, RedisCachesBuildTimeConfig buildConfig) {
+ Map resolvedValuesTypesFromAnnotations = valueTypesFromCacheResultAnnotation(combinedIndex);
+
+ Map valueTypes = new HashMap<>();
+ Optional defaultValueType = buildConfig.defaultConfig.valueType;
+ Set cacheNames = cacheNamesBuildItem.getNames();
+ for (String cacheName : cacheNames) {
+ String valueType = null;
+ RedisCacheBuildTimeConfig cacheSpecificGroup = buildConfig.cachesConfig.get(cacheName);
+ if (cacheSpecificGroup == null) {
+ if (defaultValueType.isPresent()) {
+ valueType = defaultValueType.get();
+ }
+ } else {
+ if (cacheSpecificGroup.valueType.isPresent()) {
+ valueType = cacheSpecificGroup.valueType.get();
+ }
+ }
+
+ if (valueType == null) { // TODO: does it make sense to use the return type of method annotated with @CacheResult as the last resort or should it override the default cache config?
+ valueType = resolvedValuesTypesFromAnnotations.get(cacheName);
+ }
+
+ if (valueType != null) {
+ valueTypes.put(cacheName, valueType);
+ } else {
+ throw new DeploymentException("Unable to determine the value type for '" + cacheName
+ + "' Redis cache. An appropriate configuration value for 'quarkus.cache.redis." + cacheName
+ + ".value-type' needs to be set");
+ }
+ }
+ recorder.setCacheValueTypes(valueTypes);
+ }
+
+ @NotNull
+ private static Map valueTypesFromCacheResultAnnotation(CombinedIndexBuildItem combinedIndex) {
+ Map> valueTypesFromAnnotations = new HashMap<>();
+
+ // first go through @CacheResult instances and simply record the return types
+ for (AnnotationInstance instance : combinedIndex.getIndex().getAnnotations(CacheDeploymentConstants.CACHE_RESULT)) {
+ if (instance.target().kind() != METHOD) {
+ continue;
+ }
+ Type methodReturnType = instance.target().asMethod().returnType();
+ if (methodReturnType.kind() == Type.Kind.VOID) {
+ continue;
+ }
+ AnnotationValue cacheNameValue = instance.value("cacheName");
+ if (cacheNameValue == null) {
+ continue;
+ }
+ String cacheName = cacheNameValue.asString();
+ Set types = valueTypesFromAnnotations.get(cacheName);
+ if (types == null) {
+ types = new HashSet<>(1);
+ valueTypesFromAnnotations.put(cacheName, types);
+ }
+ types.add(methodReturnType);
+ }
+
+ if (valueTypesFromAnnotations.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ Map result = new HashMap<>();
+
+ // now apply our resolution logic on the obtained types
+ for (var entry : valueTypesFromAnnotations.entrySet()) {
+ String cacheName = entry.getKey();
+ Set typeSet = entry.getValue();
+ if (typeSet.size() != 1) {
+ LOGGER.debugv("Cache named '{0}' is used on methods with different result types", cacheName);
+ // TODO: when there are multiple types for the same @CacheResult, should we fail? Should we try and be smarter in determining the type?
+ continue;
+ }
+
+ Type type = typeSet.iterator().next();
+ String resolvedType = null;
+ if (type.kind() == Type.Kind.CLASS) {
+ resolvedType = type.asClassType().name().toString();
+ } else if (type.kind() == Type.Kind.PRIMITIVE) {
+ resolvedType = type.asPrimitiveType().name().toString();
+ } else if ((type.kind() == Type.Kind.PARAMETERIZED_TYPE) && UNI.equals(type.name())) {
+ ParameterizedType parameterizedType = type.asParameterizedType();
+ List arguments = parameterizedType.arguments();
+ if (arguments.size() == 1) {
+ resolvedType = arguments.get(0).name().toString();
+ }
+ }
+
+ if (resolvedType != null) {
+ result.put(cacheName, resolvedType);
+ } else {
+ LOGGER.debugv(
+ "Cache named '{0}' is used on method whose return type '{1}' is not eligible for automatic resolution",
+ cacheName, type);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/BasicRedisCacheTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/BasicRedisCacheTest.java
new file mode 100644
index 0000000000000..67a08643f0683
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/BasicRedisCacheTest.java
@@ -0,0 +1,137 @@
+package io.quarkus.cache.redis.deployment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.inject.Inject;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.cache.Cache;
+import io.quarkus.cache.CacheManager;
+import io.quarkus.cache.redis.runtime.RedisCache;
+import io.quarkus.redis.datasource.RedisDataSource;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class BasicRedisCacheTest {
+
+ private static final String KEY_1 = "1";
+ private static final String KEY_2 = "2";
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClasses(SimpleCachedService.class, TestUtil.class));
+
+ @Inject
+ SimpleCachedService simpleCachedService;
+
+ @Test
+ public void testTypes() {
+ CacheManager cacheManager = Arc.container().select(CacheManager.class).get();
+ assertNotNull(cacheManager);
+
+ Optional cacheOpt = cacheManager.getCache(SimpleCachedService.CACHE_NAME);
+ assertTrue(cacheOpt.isPresent());
+
+ Cache cache = cacheOpt.get();
+ assertTrue(cache instanceof RedisCache);
+ }
+
+ @Test
+ public void testAllCacheAnnotations() {
+ RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get();
+ List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource);
+
+ // STEP 1
+ // Action: @CacheResult-annotated method call.
+ // Expected effect: method invoked and result cached.
+ // Verified by: STEP 2.
+ String value1 = simpleCachedService.cachedMethod(KEY_1);
+ List newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 1, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCacheKey(KEY_1));
+
+ // STEP 2
+ // Action: same call as STEP 1.
+ // Expected effect: method not invoked and result coming from the cache.
+ // Verified by: same object reference between STEPS 1 and 2 results.
+ String value2 = simpleCachedService.cachedMethod(KEY_1);
+ assertEquals(value1, value2);
+ assertEquals(allKeysAtStart.size() + 1,
+ TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 3
+ // Action: same call as STEP 2 with a new key.
+ // Expected effect: method invoked and result cached.
+ // Verified by: different objects references between STEPS 2 and 3 results.
+ String value3 = simpleCachedService.cachedMethod(KEY_2);
+ assertNotEquals(value2, value3);
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 2, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCacheKey(KEY_1), expectedCacheKey(KEY_2));
+
+ // STEP 4
+ // Action: cache entry invalidation.
+ // Expected effect: STEP 2 cache entry removed.
+ // Verified by: STEP 5.
+ simpleCachedService.invalidate(KEY_1);
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 1, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCacheKey(KEY_2)).doesNotContain(expectedCacheKey(KEY_1));
+
+ // STEP 5
+ // Action: same call as STEP 2.
+ // Expected effect: method invoked because of STEP 4 and result cached.
+ // Verified by: different objects references between STEPS 2 and 5 results.
+ String value5 = simpleCachedService.cachedMethod(KEY_1);
+ assertNotEquals(value2, value5);
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 2, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCacheKey(KEY_1), expectedCacheKey(KEY_2));
+
+ // STEP 6
+ // Action: same call as STEP 3.
+ // Expected effect: method not invoked and result coming from the cache.
+ // Verified by: same object reference between STEPS 3 and 6 results.
+ String value6 = simpleCachedService.cachedMethod(KEY_2);
+ assertEquals(value3, value6);
+ assertEquals(allKeysAtStart.size() + 2,
+ TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 7
+ // Action: full cache invalidation.
+ // Expected effect: empty cache.
+ // Verified by: STEPS 8 and 9.
+ simpleCachedService.invalidateAll();
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size(), newKeys.size());
+ Assertions.assertThat(newKeys).doesNotContain(expectedCacheKey(KEY_1), expectedCacheKey(KEY_2));
+
+ // STEP 8
+ // Action: same call as STEP 5.
+ // Expected effect: method invoked because of STEP 7 and result cached.
+ // Verified by: different objects references between STEPS 5 and 8 results.
+ String value8 = simpleCachedService.cachedMethod(KEY_1);
+ assertNotEquals(value5, value8);
+
+ // STEP 9
+ // Action: same call as STEP 6.
+ // Expected effect: method invoked because of STEP 7 and result cached.
+ // Verified by: different objects references between STEPS 6 and 9 results.
+ String value9 = simpleCachedService.cachedMethod(KEY_2);
+ assertNotEquals(value6, value9);
+ }
+
+ private static String expectedCacheKey(String key) {
+ return "cache:" + SimpleCachedService.CACHE_NAME + ":" + key;
+ }
+
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ErroneousCacheTypeTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ErroneousCacheTypeTest.java
new file mode 100644
index 0000000000000..6a954ddf857a9
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ErroneousCacheTypeTest.java
@@ -0,0 +1,16 @@
+package io.quarkus.cache.redis.deployment;
+
+import jakarta.enterprise.inject.spi.DeploymentException;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class ErroneousCacheTypeTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClass(SimpleCachedService.class))
+ .overrideConfigKey("quarkus.cache.type", "not-redis")
+ .setExpectedException(DeploymentException.class);
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/MultipleCachesTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/MultipleCachesTest.java
new file mode 100644
index 0000000000000..f96c9312435a4
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/MultipleCachesTest.java
@@ -0,0 +1,88 @@
+package io.quarkus.cache.redis.deployment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.cache.CacheResult;
+import io.quarkus.redis.datasource.RedisDataSource;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class MultipleCachesTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClasses(CachedService.class, TestUtil.class))
+ .overrideConfigKey("quarkus.cache.redis.cache2.prefix", "dummy");
+
+ @Inject
+ CachedService cachedService;
+
+ @Test
+ public void test() {
+ RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get();
+ List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource);
+
+ String key1FromCache1 = cachedService.cache1("1");
+ assertEquals(key1FromCache1, cachedService.cache1("1"));
+ List newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 1, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCache1Key("1"));
+
+ String key2FromCache1 = cachedService.cache1("2");
+ assertNotEquals(key2FromCache1, key1FromCache1);
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 2,
+ newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCache1Key("1"), expectedCache1Key("2"));
+
+ Double key1FromCache2 = cachedService.cache2(1d);
+ assertEquals(key1FromCache2, cachedService.cache2(1d));
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 3, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCache1Key("1"), expectedCache1Key("2"), expectedCache2Key("1.0"));
+
+ Double key2FromCache2 = cachedService.cache2(2d);
+ assertNotEquals(key2FromCache2, key1FromCache2);
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 4, newKeys.size());
+ Assertions.assertThat(newKeys).contains(expectedCache1Key("1"), expectedCache1Key("2"), expectedCache2Key("1.0"),
+ expectedCache2Key("2.0"));
+ }
+
+ private static String expectedCache1Key(String key) {
+ return "cache:" + CachedService.CACHE1 + ":" + key;
+ }
+
+ private static String expectedCache2Key(String key) {
+ return "dummy:" + key;
+ }
+
+ @ApplicationScoped
+ public static class CachedService {
+
+ static final String CACHE1 = "cache1";
+ static final String CACHE2 = "cache2";
+
+ @CacheResult(cacheName = CACHE1)
+ public String cache1(String key) {
+ return UUID.randomUUID().toString();
+ }
+
+ @CacheResult(cacheName = CACHE2)
+ public double cache2(double in) {
+ return ThreadLocalRandom.current().nextDouble();
+ }
+ }
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/NamedRedisCacheTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/NamedRedisCacheTest.java
new file mode 100644
index 0000000000000..5e03be1ef6e8d
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/NamedRedisCacheTest.java
@@ -0,0 +1,108 @@
+package io.quarkus.cache.redis.deployment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.List;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.redis.client.RedisClientName;
+import io.quarkus.redis.datasource.RedisDataSource;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.QuarkusTestResource;
+
+@QuarkusTestResource(RedisTestResource.class)
+public class NamedRedisCacheTest {
+
+ private static final String KEY_1 = "1";
+ private static final String KEY_2 = "2";
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClasses(SimpleCachedService.class, TestUtil.class))
+ .overrideConfigKey("quarkus.redis.test.hosts", "${quarkus.redis.tr}/1")
+ .overrideConfigKey("quarkus.cache.redis.client-name", "test");
+
+ @Inject
+ SimpleCachedService simpleCachedService;
+
+ @Test
+ public void testAllCacheAnnotations() {
+ RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class,
+ RedisClientName.Literal.of("test")).get();
+ List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource);
+
+ // STEP 1
+ // Action: @CacheResult-annotated method call.
+ // Expected effect: method invoked and result cached.
+ // Verified by: STEP 2.
+ String value1 = simpleCachedService.cachedMethod(KEY_1);
+ assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 2
+ // Action: same call as STEP 1.
+ // Expected effect: method not invoked and result coming from the cache.
+ // Verified by: same object reference between STEPS 1 and 2 results.
+ String value2 = simpleCachedService.cachedMethod(KEY_1);
+ assertEquals(value1, value2);
+ assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 3
+ // Action: same call as STEP 2 with a new key.
+ // Expected effect: method invoked and result cached.
+ // Verified by: different objects references between STEPS 2 and 3 results.
+ String value3 = simpleCachedService.cachedMethod(KEY_2);
+ assertNotEquals(value2, value3);
+ assertEquals(allKeysAtStart.size() + 2, TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 4
+ // Action: cache entry invalidation.
+ // Expected effect: STEP 2 cache entry removed.
+ // Verified by: STEP 5.
+ simpleCachedService.invalidate(KEY_1);
+ assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 5
+ // Action: same call as STEP 2.
+ // Expected effect: method invoked because of STEP 4 and result cached.
+ // Verified by: different objects references between STEPS 2 and 5 results.
+ String value5 = simpleCachedService.cachedMethod(KEY_1);
+ assertNotEquals(value2, value5);
+ assertEquals(allKeysAtStart.size() + 2, TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 6
+ // Action: same call as STEP 3.
+ // Expected effect: method not invoked and result coming from the cache.
+ // Verified by: same object reference between STEPS 3 and 6 results.
+ String value6 = simpleCachedService.cachedMethod(KEY_2);
+ assertEquals(value3, value6);
+ assertEquals(allKeysAtStart.size() + 2, TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 7
+ // Action: full cache invalidation.
+ // Expected effect: empty cache.
+ // Verified by: STEPS 8 and 9.
+ simpleCachedService.invalidateAll();
+ assertEquals(allKeysAtStart.size(), TestUtil.allRedisKeys(redisDataSource).size());
+
+ // STEP 8
+ // Action: same call as STEP 5.
+ // Expected effect: method invoked because of STEP 7 and result cached.
+ // Verified by: different objects references between STEPS 5 and 8 results.
+ String value8 = simpleCachedService.cachedMethod(KEY_1);
+ assertNotEquals(value5, value8);
+
+ // STEP 9
+ // Action: same call as STEP 6.
+ // Expected effect: method invoked because of STEP 7 and result cached.
+ // Verified by: different objects references between STEPS 6 and 9 results.
+ String value9 = simpleCachedService.cachedMethod(KEY_2);
+ assertNotEquals(value6, value9);
+ }
+
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/PojoAndMultipleKeysCacheTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/PojoAndMultipleKeysCacheTest.java
new file mode 100644
index 0000000000000..758ac269c430d
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/PojoAndMultipleKeysCacheTest.java
@@ -0,0 +1,107 @@
+package io.quarkus.cache.redis.deployment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.cache.CacheKey;
+import io.quarkus.cache.CacheResult;
+import io.quarkus.redis.datasource.RedisDataSource;
+import io.quarkus.test.QuarkusUnitTest;
+import io.smallrye.mutiny.Uni;
+
+public class PojoAndMultipleKeysCacheTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClasses(CachedService.class, Message.class, TestUtil.class));
+
+ @Inject
+ CachedService cachedService;
+
+ @Test
+ public void test() {
+ RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get();
+ List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource);
+
+ Message messageFromKey1 = cachedService.getMessage("hello", "a", "1").await().indefinitely();
+ List newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 1, newKeys.size());
+
+ assertEquals(messageFromKey1, cachedService.getMessage("hello", "b", "1").await().indefinitely());
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 1, newKeys.size());
+
+ Message messageFromKey2 = cachedService.getMessage("world", "c", "1").await().indefinitely();
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 2, newKeys.size());
+
+ assertEquals(messageFromKey2, cachedService.getMessage("world", "d", "1").await().indefinitely());
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 2, newKeys.size());
+
+ Message otherMessageFromKey1 = cachedService.getMessage("hello", "e", "2").await().indefinitely();
+ assertNotEquals(otherMessageFromKey1, messageFromKey1);
+ assertEquals(otherMessageFromKey1, cachedService.getMessage("hello", "f", "2").await().indefinitely());
+ newKeys = TestUtil.allRedisKeys(redisDataSource);
+ assertEquals(allKeysAtStart.size() + 3, newKeys.size());
+ }
+
+ @Singleton
+ public static class CachedService {
+
+ @CacheResult(cacheName = "message")
+ public Uni getMessage(@CacheKey String key, String notPartOfTheKey, @CacheKey String otherKey) {
+ return Uni.createFrom().item(new Message(UUID.randomUUID().toString(), ThreadLocalRandom.current().nextInt()));
+ }
+ }
+
+ public static class Message {
+ private final String str;
+ private final int num;
+
+ @JsonCreator
+ public Message(String str, int num) {
+ this.str = str;
+ this.num = num;
+ }
+
+ public String getStr() {
+ return str;
+ }
+
+ public int getNum() {
+ return num;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Message message = (Message) o;
+ return num == message.num && str.equals(message.str);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(str, num);
+ }
+ }
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/RedisTestResource.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/RedisTestResource.java
new file mode 100644
index 0000000000000..d680374ad3fc4
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/RedisTestResource.java
@@ -0,0 +1,30 @@
+package io.quarkus.cache.redis.deployment;
+
+import java.util.Map;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+
+public class RedisTestResource implements QuarkusTestResourceLifecycleManager {
+
+ static GenericContainer> server = new GenericContainer<>(
+ DockerImageName.parse("redis:7-alpine"))
+ .withExposedPorts(6379);
+
+ @Override
+ public Map start() {
+ server.start();
+ return Map.of("quarkus.redis.tr", getEndpoint());
+ }
+
+ @Override
+ public void stop() {
+ server.stop();
+ }
+
+ public static String getEndpoint() {
+ return String.format("redis://%s:%s", server.getHost(), server.getMappedPort(6379));
+ }
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/SimpleCachedService.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/SimpleCachedService.java
new file mode 100644
index 0000000000000..88b641e0784d1
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/SimpleCachedService.java
@@ -0,0 +1,28 @@
+package io.quarkus.cache.redis.deployment;
+
+import java.util.UUID;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.quarkus.cache.CacheInvalidate;
+import io.quarkus.cache.CacheInvalidateAll;
+import io.quarkus.cache.CacheResult;
+
+@ApplicationScoped
+public class SimpleCachedService {
+
+ static final String CACHE_NAME = "test-cache";
+
+ @CacheResult(cacheName = CACHE_NAME)
+ public String cachedMethod(String key) {
+ return UUID.randomUUID().toString();
+ }
+
+ @CacheInvalidate(cacheName = CACHE_NAME)
+ public void invalidate(String key) {
+ }
+
+ @CacheInvalidateAll(cacheName = CACHE_NAME)
+ public void invalidateAll() {
+ }
+}
diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/TestUtil.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/TestUtil.java
new file mode 100644
index 0000000000000..2eb746715ce5f
--- /dev/null
+++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/TestUtil.java
@@ -0,0 +1,22 @@
+package io.quarkus.cache.redis.deployment;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import io.quarkus.redis.datasource.RedisDataSource;
+
+final class TestUtil {
+
+ private TestUtil() {
+ }
+
+ static List allRedisKeys(RedisDataSource redisDataSource) {
+ Iterator iter = redisDataSource.key().scan().toIterable().iterator();
+ List result = new ArrayList<>();
+ while (iter.hasNext()) {
+ result.add(iter.next());
+ }
+ return result;
+ }
+}
diff --git a/extensions/redis-cache/pom.xml b/extensions/redis-cache/pom.xml
new file mode 100644
index 0000000000000..e1fae6a1e5fb4
--- /dev/null
+++ b/extensions/redis-cache/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+
+
+ quarkus-extensions-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-redis-cache-parent
+ pom
+
+ Quarkus - Redis Cache
+
+
+ deployment
+ runtime
+
+
+
+
diff --git a/extensions/redis-cache/runtime/pom.xml b/extensions/redis-cache/runtime/pom.xml
new file mode 100644
index 0000000000000..860e13e5aa0be
--- /dev/null
+++ b/extensions/redis-cache/runtime/pom.xml
@@ -0,0 +1,113 @@
+
+
+ 4.0.0
+
+
+ io.quarkus
+ quarkus-redis-cache-parent
+ 999-SNAPSHOT
+
+
+ quarkus-redis-cache
+
+ Quarkus - Redis Cache - Runtime
+ Use Redis as the caching backend
+
+
+ io.quarkus
+ quarkus-redis-client
+
+
+ io.quarkus
+ quarkus-cache
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ io.quarkus
+ quarkus-junit4-mock
+ test
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ junit
+ junit
+
+
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+
+ test-redis
+
+
+ test-containers
+
+
+
+
+
+ maven-surefire-plugin
+
+ false
+
+
+
+ maven-failsafe-plugin
+
+ false
+
+
+
+
+
+
+
diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java
new file mode 100644
index 0000000000000..8127fde03e743
--- /dev/null
+++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCache.java
@@ -0,0 +1,89 @@
+package io.quarkus.cache.redis.runtime;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import io.quarkus.cache.Cache;
+import io.smallrye.mutiny.Uni;
+
+public interface RedisCache extends Cache {
+
+ /**
+ * When configured, gets the default type of the value stored in the cache.
+ * The configured type is used when no type is passed into the {@link #get(Object, Class, Function)}.
+ *
+ * @return the type, {@code null} if not configured.
+ */
+ Class> getDefaultValueType();
+
+ @Override
+ default Uni get(K key, Function valueLoader) {
+ Class type = (Class) getDefaultValueType();
+ if (type == null) {
+ throw new UnsupportedOperationException("Cannot use `get` method without a default type configured. " +
+ "Consider using the `get` method accepting the type or configure the default type for the cache " +
+ getName());
+ }
+ return get(key, type, valueLoader);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ default Uni getAsync(K key, Function> valueLoader) {
+ Class type = (Class) getDefaultValueType();
+ if (type == null) {
+ throw new UnsupportedOperationException("Cannot use `getAsync` method without a default type configured. " +
+ "Consider using the `getAsync` method accepting the type or configure the default type for the cache " +
+ getName());
+ }
+ return getAsync(key, type, valueLoader);
+ }
+
+ /**
+ * Allows retrieving a value from the Redis cache.
+ *
+ * @param key the key
+ * @param clazz the class of the value
+ * @param valueLoader the value loader called when there is no value stored in the cache
+ * @param the type of key
+ * @param the type of value
+ * @return the Uni emitting the cached value.
+ */
+ Uni get(K key, Class clazz, Function valueLoader);
+
+ /**
+ * Allows retrieving a value from the Redis cache.
+ *
+ * @param key the key
+ * @param clazz the class of the value
+ * @param valueLoader the value loader called when there is no value stored in the cache
+ * @param the type of key
+ * @param the type of value
+ * @return the Uni emitting the cached value.
+ */
+ Uni getAsync(K key, Class clazz, Function> valueLoader);
+
+ /**
+ * Put a value in the cache.
+ *
+ * @param key the key
+ * @param value the value
+ * @param the type of key
+ * @param the type of value
+ * @return a Uni emitting {@code null} when the operation completes
+ */
+ default Uni put(K key, V value) {
+ return put(key, new Supplier() {
+ @Override
+ public V get() {
+ return value;
+ }
+ });
+ }
+
+ Uni put(K key, Supplier supplier);
+
+ Uni getOrDefault(K key, V defaultValue);
+
+ Uni getOrNull(K key, Class clazz);
+}
diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java
new file mode 100644
index 0000000000000..d4c95091d5680
--- /dev/null
+++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildRecorder.java
@@ -0,0 +1,72 @@
+package io.quarkus.cache.redis.runtime;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.cache.Cache;
+import io.quarkus.cache.CacheManager;
+import io.quarkus.cache.CacheManagerInfo;
+import io.quarkus.cache.runtime.CacheManagerImpl;
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+
+@Recorder
+public class RedisCacheBuildRecorder {
+
+ private static final Logger LOGGER = Logger.getLogger(RedisCacheBuildRecorder.class);
+
+ private final RedisCachesBuildTimeConfig buildConfig;
+ private final RuntimeValue redisCacheConfigRV;
+
+ private static Map valueTypes;
+
+ public RedisCacheBuildRecorder(RedisCachesBuildTimeConfig buildConfig, RuntimeValue redisCacheConfigRV) {
+ this.buildConfig = buildConfig;
+ this.redisCacheConfigRV = redisCacheConfigRV;
+ }
+
+ public CacheManagerInfo getCacheManagerSupplier() {
+ return new CacheManagerInfo() {
+ @Override
+ public boolean supports(Context context) {
+ return context.cacheEnabled() && "redis".equals(context.cacheType()); // TODO: fix constant
+ }
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public Supplier get(Context context) {
+ return new Supplier() {
+ @Override
+ public CacheManager get() {
+ Set cacheInfos = RedisCacheInfoBuilder.build(context.cacheNames(), buildConfig,
+ redisCacheConfigRV.getValue(), valueTypes);
+ if (cacheInfos.isEmpty()) {
+ return new CacheManagerImpl(Collections.emptyMap());
+ } else {
+ // The number of caches is known at build time so we can use fixed initialCapacity and loadFactor for the caches map.
+ Map caches = new HashMap<>(cacheInfos.size() + 1, 1.0F);
+ for (RedisCacheInfo cacheInfo : cacheInfos) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf(
+ "Building Redis cache [%s] with [ttl=%s], [prefix=%s], [classOfItems=%s]",
+ cacheInfo.name, cacheInfo.ttl, cacheInfo.prefix,
+ cacheInfo.valueType);
+ }
+
+ RedisCacheImpl cache = new RedisCacheImpl(cacheInfo, buildConfig.clientName);
+ caches.put(cacheInfo.name, cache);
+ }
+ return new CacheManagerImpl(caches);
+ }
+ }
+ };
+ }
+ };
+ }
+
+ public void setCacheValueTypes(Map valueTypes) {
+ RedisCacheBuildRecorder.valueTypes = valueTypes;
+ }
+}
diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildTimeConfig.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildTimeConfig.java
new file mode 100644
index 0000000000000..9db9560c408a7
--- /dev/null
+++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheBuildTimeConfig.java
@@ -0,0 +1,22 @@
+package io.quarkus.cache.redis.runtime;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+@ConfigGroup
+public class RedisCacheBuildTimeConfig {
+
+ /**
+ * The default type of the value stored in the cache.
+ */
+ @ConfigItem
+ public Optional valueType;
+
+ /**
+ * The key type, {@code String} by default.
+ */
+ @ConfigItem
+ public Optional keyType;
+}
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
new file mode 100644
index 0000000000000..0fa03e1c9e961
--- /dev/null
+++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java
@@ -0,0 +1,405 @@
+package io.quarkus.cache.redis.runtime;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.ArcContainer;
+import io.quarkus.cache.CacheException;
+import io.quarkus.cache.CompositeCacheKey;
+import io.quarkus.cache.runtime.AbstractCache;
+import io.quarkus.redis.client.RedisClientName;
+import io.quarkus.redis.runtime.datasource.Marshaller;
+import io.smallrye.mutiny.Uni;
+import io.smallrye.mutiny.unchecked.Unchecked;
+import io.smallrye.mutiny.unchecked.UncheckedFunction;
+import io.vertx.mutiny.redis.client.Command;
+import io.vertx.mutiny.redis.client.Redis;
+import io.vertx.mutiny.redis.client.RedisConnection;
+import io.vertx.mutiny.redis.client.Request;
+import io.vertx.mutiny.redis.client.Response;
+
+/**
+ * This class is an internal Quarkus cache implementation using Redis.
+ * Do not use it explicitly from your Quarkus application.
+ */
+public class RedisCacheImpl extends AbstractCache implements RedisCache {
+
+ private static final Map> PRIMITIVE_TO_CLASS_MAPPING = Map.of(
+ "int", Integer.class,
+ "byte", Byte.class,
+ "char", Character.class,
+ "short", Short.class,
+ "long", Long.class,
+ "float", Float.class,
+ "double", Double.class,
+ "boolean", Boolean.class);
+
+ private final Redis redis;
+
+ private final RedisCacheInfo cacheInfo;
+ private final Class classOfValue;
+ private final Class classOfKey;
+
+ private final Marshaller marshaller;
+
+ public RedisCacheImpl(RedisCacheInfo cacheInfo, Optional redisClientName) {
+
+ this(cacheInfo, determineRedisClient(redisClientName));
+ }
+
+ private static Redis determineRedisClient(Optional redisClientName) {
+ ArcContainer container = Arc.container();
+ if (redisClientName.isPresent()) {
+ return container.select(Redis.class, RedisClientName.Literal.of(redisClientName.get())).get();
+ } else {
+ return container.select(Redis.class).get();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public RedisCacheImpl(RedisCacheInfo cacheInfo, Redis redis) {
+ this.cacheInfo = cacheInfo;
+
+ try {
+ this.classOfKey = (Class) loadClass(this.cacheInfo.keyType);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Unable to load the class " + this.cacheInfo.keyType, e);
+ }
+
+ if (this.cacheInfo.valueType != null) {
+ try {
+ this.classOfValue = (Class) loadClass(this.cacheInfo.valueType);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Unable to load the class " + this.cacheInfo.valueType, e);
+ }
+ this.marshaller = new Marshaller(this.classOfValue, this.classOfKey);
+ } else {
+ this.classOfValue = null;
+ this.marshaller = new Marshaller(this.classOfKey);
+ }
+ this.marshaller.add(CompositeCacheKey.class);
+ this.redis = redis;
+ }
+
+ private Class> loadClass(String type) throws ClassNotFoundException {
+ if (PRIMITIVE_TO_CLASS_MAPPING.containsKey(type)) {
+ return PRIMITIVE_TO_CLASS_MAPPING.get(type);
+ }
+ return Thread.currentThread().getContextClassLoader().loadClass(type);
+ }
+
+ @Override
+ public String getName() {
+ return Objects.requireNonNullElse(cacheInfo.name, "default-redis-cache");
+ }
+
+ @Override
+ public Object getDefaultKey() {
+ return "default-cache-key";
+ }
+
+ @Override
+ public Class> getDefaultValueType() {
+ return classOfValue;
+ }
+
+ private String encodeKey(K key) {
+ return new String(marshaller.encode(key), StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Uni get(K key, Class clazz, Function valueLoader) {
+ // With optimistic locking:
+ // WATCH K
+ // val = deserialize(GET K)
+ // If val == null
+ // MULTI
+ // SET K computation.apply(K)
+ // EXEC
+ // Without:
+ // val = deserialize(GET K)
+ // if (val == null) => SET K computation.apply(K)
+ byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key)));
+ return withConnection(new Function>() {
+ @Override
+ public Uni apply(RedisConnection connection) {
+ return watch(connection, encodedKey)
+ .chain(new GetFromConnectionSupplier<>(connection, clazz, encodedKey, marshaller))
+ .chain(Unchecked.function(new UncheckedFunction<>() {
+ @Override
+ 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);
+ } else {
+ return set(connection, encodedKey, encodedValue).replaceWith(value);
+ }
+ }
+ }
+ }));
+ }
+ });
+ }
+
+ @Override
+ public Uni getAsync(K key, Class clazz, Function> valueLoader) {
+ byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key)));
+ return withConnection(new Function>() {
+ @Override
+ public Uni apply(RedisConnection connection) {
+ return watch(connection, encodedKey)
+ .chain(new GetFromConnectionSupplier<>(connection, clazz, encodedKey, marshaller))
+ .chain(cached -> {
+ if (cached != null) {
+ return Uni.createFrom().item(new StaticSupplier<>(cached));
+ } else {
+ Uni getter = valueLoader.apply(key);
+ return getter
+ .chain(value -> {
+ byte[] encodedValue = marshaller.encode(value);
+ if (cacheInfo.useOptimisticLocking) {
+ return multi(connection, set(connection, encodedKey, encodedValue))
+ .replaceWith(value);
+ } else {
+ return set(connection, encodedKey, encodedValue)
+ .replaceWith(value);
+ }
+ });
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public Uni put(K key, V value) {
+ return put(key, new StaticSupplier<>(value));
+ }
+
+ @Override
+ public Uni put(K key, Supplier supplier) {
+ byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key)));
+ byte[] encodedValue = marshaller.encode(supplier.get());
+ return withConnection(new Function>() {
+ @Override
+ public Uni apply(RedisConnection connection) {
+ return set(connection, encodedKey, encodedValue);
+ }
+ });
+ }
+
+ private void enforceDefaultType() {
+ if (classOfValue == null) {
+ throw new UnsupportedOperationException(
+ "Cannot execute the operation without the default type configured in cache " + cacheInfo.name);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Uni getOrDefault(K key, V defaultValue) {
+ enforceDefaultType();
+ byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key)));
+ return withConnection(new Function>() {
+ @Override
+ public Uni apply(RedisConnection redisConnection) {
+ return (Uni) doGet(redisConnection, encodedKey, classOfValue, marshaller);
+ }
+ }).onItem().ifNull().continueWith(new StaticSupplier<>(defaultValue));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Uni getOrNull(K key, Class clazz) {
+ enforceDefaultType();
+ byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key)));
+ return withConnection(new Function>() {
+ @Override
+ public Uni apply(RedisConnection redisConnection) {
+ return (Uni) doGet(redisConnection, encodedKey, classOfValue, marshaller);
+ }
+ });
+ }
+
+ @Override
+ public Uni invalidate(Object key) {
+ byte[] encodedKey = marshaller.encode(computeActualKey(encodeKey(key)));
+ return redis.send(Request.cmd(Command.DEL).arg(encodedKey))
+ .replaceWithVoid();
+ }
+
+ @Override
+ public Uni invalidateAll() {
+ return invalidateIf(AlwaysTruePredicate.INSTANCE);
+ }
+
+ @Override
+ public Uni invalidateIf(Predicate