diff --git a/extensions/cache/deployment/pom.xml b/extensions/cache/deployment/pom.xml
index 7f5f0197b7714..a1c35c5f644c4 100644
--- a/extensions/cache/deployment/pom.xml
+++ b/extensions/cache/deployment/pom.xml
@@ -26,6 +26,10 @@
io.quarkusquarkus-caffeine-deployment
+
+ io.quarkus
+ quarkus-mutiny-deployment
+
diff --git a/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/UniValueTest.java b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/UniValueTest.java
new file mode 100644
index 0000000000000..ff7e82583e1f4
--- /dev/null
+++ b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/UniValueTest.java
@@ -0,0 +1,95 @@
+package io.quarkus.cache.test.runtime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.cache.CacheResult;
+import io.quarkus.test.QuarkusUnitTest;
+import io.smallrye.mutiny.Uni;
+
+/**
+ * Tests the {@link CacheResult} annotation on methods returning {@link Uni}.
+ */
+public class UniValueTest {
+
+ private static final String KEY = "key";
+
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClass(CachedService.class).addAsResource(
+ new StringAsset("quarkus.log.category.\"io.quarkus.cache\".level=DEBUG"), "application.properties"));
+
+ @Inject
+ CachedService cachedService;
+
+ @Test
+ public void test() {
+ // STEP 1
+ // Action: a method annotated with @CacheResult and returning a Uni is called.
+ // Expected effect: the method is invoked and its result (a Uni) is wrapped into an UncomputedUniValue which is cached.
+ // Verified by: invocations counter and CacheResultInterceptor log.
+ Uni uni1 = cachedService.cachedMethod(KEY);
+ assertEquals(1, cachedService.getInvocations());
+
+ // STEP 2
+ // Action: same call as STEP 1.
+ // Expected effect: the method is not invoked and the cached UncomputedUniValue from STEP 1 is used to produce a result.
+ // Verified by: invocations counter and CacheResultInterceptor log.
+ Uni uni2 = cachedService.cachedMethod(KEY);
+ assertEquals(1, cachedService.getInvocations());
+
+ // STEP 3
+ // Action: the Uni returned in STEP 1 is subscribed to and we wait for an item event to be fired.
+ // Expected effect: the UncomputedUniValue wrapper cached during STEP 1 is replaced with the emitted item from this step in the cache.
+ // Verified by: CaffeineCache log.
+ String emittedItem1 = uni1.await().indefinitely();
+
+ // STEP 4
+ // Action: the Uni returned in STEP 2 is subscribed to and we wait for an item event to be fired.
+ // Expected effect: the emitted item from STEP 3 is replaced with the emitted item from this step in the cache.
+ // Verified by: CaffeineCache log and different objects references between STEPS 3 and 4 emitted items.
+ String emittedItem2 = uni2.await().indefinitely();
+ assertTrue(emittedItem1 != emittedItem2);
+
+ // STEP 5
+ // Action: same call as STEP 2 but we immediately subscribe to the returned Uni and wait for an item event to be fired.
+ // Expected effect: the method is not invoked and the emitted item cached during STEP 4 is returned.
+ // Verified by: invocations counter and same object reference between STEPS 4 and 5 emitted items.
+ String emittedItem3 = cachedService.cachedMethod(KEY).await().indefinitely();
+ assertEquals(1, cachedService.getInvocations());
+ assertTrue(emittedItem2 == emittedItem3);
+
+ // STEP 6
+ // Action: same call as STEP 5 with a different key.
+ // Expected effect: the method is invoked and its result (a Uni) is wrapped into an UncomputedUniValue which is cached.
+ // Verified by: invocations counter, CacheResultInterceptor log and different objects references between STEPS 5 and 6 emitted items.
+ String emittedItem4 = cachedService.cachedMethod("another-key").await().indefinitely();
+ assertEquals(2, cachedService.getInvocations());
+ assertTrue(emittedItem3 != emittedItem4);
+ }
+
+ @ApplicationScoped
+ static class CachedService {
+
+ private int invocations;
+
+ @CacheResult(cacheName = "test-cache")
+ public Uni cachedMethod(String key) {
+ invocations++;
+ return Uni.createFrom().item(() -> new String());
+ }
+
+ public int getInvocations() {
+ return invocations;
+ }
+ }
+}
diff --git a/extensions/cache/runtime/pom.xml b/extensions/cache/runtime/pom.xml
index 73dd9f19a3744..4c2bddc8d4948 100644
--- a/extensions/cache/runtime/pom.xml
+++ b/extensions/cache/runtime/pom.xml
@@ -23,6 +23,10 @@
io.quarkusquarkus-caffeine
+
+ io.quarkus
+ quarkus-mutiny
+ io.vertxvertx-web
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/AbstractCache.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/AbstractCache.java
index 8545d93c1b95a..d489be3811151 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/AbstractCache.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/AbstractCache.java
@@ -4,6 +4,7 @@
import java.util.function.Function;
import io.quarkus.cache.Cache;
+import io.smallrye.mutiny.Uni;
public abstract class AbstractCache implements Cache {
@@ -32,4 +33,17 @@ public Object getDefaultKey() {
public abstract void invalidate(Object key);
public abstract void invalidateAll();
+
+ /**
+ * Replaces the cache value associated with the given key by an item emitted by a Uni. This method can be called several
+ * times for the same key, each call will then always replace the existing cache entry with the given emitted value. If the
+ * key no longer identifies a cache entry, this method must not put the emitted item into the cache.
+ */
+ public abstract Uni replaceUniValue(Object key, Object emittedValue);
+
+ /**
+ * Removes the cache entry identified by the given key only if the cache value is the given {@link UncomputedUniValue}.
+ * This method is called in case of failure during the computation of a cached {@link Uni}.
+ */
+ public abstract Uni removeUncomputedUniValue(Object key, UncomputedUniValue uncomputedUniValue);
}
diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java
index 60d19de8fd61d..85435a52ccea2 100644
--- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java
+++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java
@@ -14,6 +14,7 @@
import org.jboss.logging.Logger;
import io.quarkus.cache.CacheResult;
+import io.smallrye.mutiny.Uni;
@CacheResult(cacheName = "") // The `cacheName` attribute is @Nonbinding.
@Interceptor
@@ -47,28 +48,42 @@ public Object intercept(InvocationContext invocationContext) throws Throwable {
@Override
public Object apply(Object k) {
try {
- return invocationContext.proceed();
+ Object invocationResult = invocationContext.proceed();
+ if (invocationResult instanceof Uni) {
+ LOGGER.debugf("Adding UncomputedUniValue entry with key [%s] into cache [%s]", key,
+ cache.getName());
+ return new UncomputedUniValue((Uni