diff --git a/README.md b/README.md index 75a0b5006..792d9e22f 100644 --- a/README.md +++ b/README.md @@ -1114,6 +1114,16 @@ It covers different usages: 3. from a blocking endpoint 4. from a reactive endpoint +### `cache/infinispan` +Verifies the `quarkus-infinispan-cache` extension using `@CacheResult`, `@CacheInvalidate`, `@CacheInvalidateAll` and `@CacheKey`. +It covers different usages: +1. from an application scoped service +2. from a request scoped service +3. from a blocking endpoint +4. from a reactive endpoint + +Also test POJOs as cache value and cache expiration. + ### `cache/redis` Verifies the `quarkus-redis-cache` extension using `@CacheResult`, `@CacheInvalidate`, `@CacheInvalidateAll` and `@CacheKey`. diff --git a/cache/infinispan/pom.xml b/cache/infinispan/pom.xml new file mode 100644 index 000000000..e94f3830f --- /dev/null +++ b/cache/infinispan/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../.. + + cache-infinispan + jar + Quarkus QE TS: Cache: Infinispan + + + io.quarkus + quarkus-infinispan-cache + + + io.quarkus + quarkus-rest + + + io.quarkus.qe + quarkus-test-service-infinispan + test + + + diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ApplicationScopeService.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ApplicationScopeService.java new file mode 100644 index 000000000..88aef9fe0 --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ApplicationScopeService.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.infinispan; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ApplicationScopeService extends BaseServiceWithCache { +} diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/BaseServiceWithCache.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/BaseServiceWithCache.java new file mode 100644 index 000000000..2484dd9a0 --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/BaseServiceWithCache.java @@ -0,0 +1,52 @@ +package io.quarkus.ts.cache.infinispan; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.infinispan.protostream.GeneratedSchema; +import org.infinispan.protostream.annotations.Proto; +import org.infinispan.protostream.annotations.ProtoSchema; + +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 final AtomicInteger counter = new AtomicInteger(0); + + @CacheResult(cacheName = CACHE_NAME) + public String getValue() { + return "Value: " + counter.getAndIncrement(); + } + + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidate() { + // do nothing + } + + @CacheResult(cacheName = CACHE_NAME) + public ExpensiveResponse getValueWithPrefix(@CacheKey String prefix) { + return new ExpensiveResponse(prefix + ": " + counter.getAndIncrement()); + } + + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidateWithPrefix(@CacheKey String prefix) { + // do nothing + } + + @CacheInvalidateAll(cacheName = CACHE_NAME) + public void invalidateAll() { + // do nothing + } + + @Proto + public record ExpensiveResponse(String result) { + } + + @ProtoSchema(includeClasses = { ExpensiveResponse.class }) + interface Schema extends GeneratedSchema { + } +} diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/BlockingWithCacheResource.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/BlockingWithCacheResource.java new file mode 100644 index 000000000..0283b75af --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/BlockingWithCacheResource.java @@ -0,0 +1,57 @@ +package io.quarkus.ts.cache.infinispan; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; +import io.smallrye.common.annotation.Blocking; + +@Blocking +@Path("/api/blocking") +public class BlockingWithCacheResource { + + private static final String CACHE_NAME = "api-blocking-cache"; + + private final AtomicInteger counter = new AtomicInteger(0); + + @GET + @CacheResult(cacheName = CACHE_NAME) + public String getValue() { + return "Value: " + counter.getAndIncrement(); + } + + @POST + @Path("/invalidate-cache") + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidate() { + // do nothing + } + + @GET + @Path("/using-prefix/{prefix}") + @CacheResult(cacheName = CACHE_NAME) + public String getValueWithPrefix(@PathParam("prefix") @CacheKey String prefix) { + return prefix + ": " + counter.getAndIncrement(); + } + + @POST + @Path("/using-prefix/{prefix}/invalidate-cache") + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidateWithPrefix(@PathParam("prefix") @CacheKey String prefix) { + // do nothing + } + + @POST + @Path("/invalidate-cache-all") + @CacheInvalidateAll(cacheName = CACHE_NAME) + public void invalidateAll() { + // do nothing + } +} diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/CacheExpirationResource.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/CacheExpirationResource.java new file mode 100644 index 000000000..98823bd57 --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/CacheExpirationResource.java @@ -0,0 +1,21 @@ +package io.quarkus.ts.cache.infinispan; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; + +@Path("/cache") +public class CacheExpirationResource { + + @GET + @Path("/{key}") + @Produces(MediaType.TEXT_PLAIN) + @CacheResult(cacheName = "expiring-cache") + public String getCachedValue(@CacheKey String key) { + return "Value for key " + key + " at " + System.currentTimeMillis(); + } +} diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ReactiveWithCacheResource.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ReactiveWithCacheResource.java new file mode 100644 index 000000000..85989af67 --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ReactiveWithCacheResource.java @@ -0,0 +1,58 @@ +package io.quarkus.ts.cache.infinispan; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.mutiny.Uni; + +@NonBlocking +@Path("/api/reactive") +public class ReactiveWithCacheResource { + + private static final String CACHE_NAME = "api-reactive-cache"; + + private final AtomicInteger counter = new AtomicInteger(0); + + @GET + @CacheResult(cacheName = CACHE_NAME) + public Uni getValue() { + return Uni.createFrom().item("Value: " + counter.getAndIncrement()); + } + + @POST + @Path("/invalidate-cache") + @CacheInvalidate(cacheName = CACHE_NAME) + public Uni invalidate() { + return Uni.createFrom().nullItem(); + } + + @GET + @Path("/using-prefix/{prefix}") + @CacheResult(cacheName = CACHE_NAME) + public Uni getValueWithPrefix(@PathParam("prefix") @CacheKey String prefix) { + return Uni.createFrom().item(prefix + ": " + counter.getAndIncrement()); + } + + @POST + @Path("/using-prefix/{prefix}/invalidate-cache") + @CacheInvalidate(cacheName = CACHE_NAME) + public Uni invalidateWithPrefix(@PathParam("prefix") @CacheKey String prefix) { + return Uni.createFrom().nullItem(); + } + + @POST + @Path("/invalidate-cache-all") + @CacheInvalidateAll(cacheName = CACHE_NAME) + public Uni invalidateAll() { + return Uni.createFrom().nullItem(); + } +} diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/RequestScopeService.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/RequestScopeService.java new file mode 100644 index 000000000..8828dad0c --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/RequestScopeService.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.infinispan; + +import jakarta.enterprise.context.RequestScoped; + +@RequestScoped +public class RequestScopeService extends BaseServiceWithCache { +} diff --git a/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ServiceWithCacheResource.java b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ServiceWithCacheResource.java new file mode 100644 index 000000000..3ad935326 --- /dev/null +++ b/cache/infinispan/src/main/java/io/quarkus/ts/cache/infinispan/ServiceWithCacheResource.java @@ -0,0 +1,66 @@ +package io.quarkus.ts.cache.infinispan; + +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 BaseServiceWithCache.ExpensiveResponse 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/infinispan/src/main/resources/application.properties b/cache/infinispan/src/main/resources/application.properties new file mode 100644 index 000000000..a3c0fad0f --- /dev/null +++ b/cache/infinispan/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.infinispan-client.hosts=localhost:11222 +quarkus.cache.infinispan.expiring-cache.lifespan=3s +quarkus.cache.infinispan.expiring-cache.max-idle=5s diff --git a/cache/infinispan/src/test/java/io/quarkus/ts/cache/infinispan/InfinispanCacheIT.java b/cache/infinispan/src/test/java/io/quarkus/ts/cache/infinispan/InfinispanCacheIT.java new file mode 100644 index 000000000..82c1ffc0a --- /dev/null +++ b/cache/infinispan/src/test/java/io/quarkus/ts/cache/infinispan/InfinispanCacheIT.java @@ -0,0 +1,215 @@ +package io.quarkus.ts.cache.infinispan; + +import static io.quarkus.ts.cache.infinispan.ServiceWithCacheResource.APPLICATION_SCOPE_SERVICE_PATH; +import static io.quarkus.ts.cache.infinispan.ServiceWithCacheResource.REQUEST_SCOPE_SERVICE_PATH; +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.InfinispanService; +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 InfinispanCacheIT { + + 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 RESOURCE_BLOCKING_API_PATH = "/api/blocking"; + private static final String RESOURCE_REACTIVE_API_PATH = "/api/reactive"; + + private static final String PREFIX_ONE = "prefix1"; + private static final String PREFIX_TWO = "prefix2"; + + private static final int INFINISPAN_PORT = 11222; + + @Container(image = "${infinispan.image}", expectedLog = "${infinispan.expected-log}", port = INFINISPAN_PORT) + static InfinispanService infinispan = new InfinispanService() + .withUsername("admin") + .withPassword("password"); + + @QuarkusApplication() + static RestService app = new RestService() + .withProperty("quarkus.infinispan-client.hosts", infinispan::getInfinispanServerAddress) + .withProperties("test.properties"); + + /** + * Check whether the `@CacheResult` annotation works when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH, RESOURCE_BLOCKING_API_PATH, + RESOURCE_REACTIVE_API_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, RESOURCE_BLOCKING_API_PATH, + RESOURCE_REACTIVE_API_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, RESOURCE_BLOCKING_API_PATH, + RESOURCE_REACTIVE_API_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, RESOURCE_BLOCKING_API_PATH, + RESOURCE_REACTIVE_API_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, RESOURCE_BLOCKING_API_PATH, + RESOURCE_REACTIVE_API_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, RESOURCE_BLOCKING_API_PATH, + RESOURCE_REACTIVE_API_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)); + } + + @Test + public void testCacheLifespan() throws InterruptedException { + // First request, value is cached + String firstResponse = getFromPath("/cache/testLifespan"); + + // Wait for 3 seconds and make another request, cache should return a different result + Thread.sleep(3000); + + String secondResponse = getFromPath("/cache/testLifespan"); + + assertNotEquals(firstResponse, secondResponse, "Cache should return a new value after expiration"); + } + + @Test + public void testCacheMaxIdle() throws InterruptedException { + // First request + String firstResponse = getFromPath("/cache/testIdle"); + + // Wait for 2 seconds and make another request, cache should return the same result + Thread.sleep(2000); + + String secondResponse = getFromPath("/cache/testIdle"); + + assertEquals(firstResponse, secondResponse, "Cache should hold the same value within the lifespan and max-idle"); + + // Wait for 5 more seconds and make another request, cache should return a different result + Thread.sleep(5000); + + String thirdResponse = getFromPath("/cache/testIdle"); + + assertNotEquals(firstResponse, thirdResponse, "Cache should return a new value after expiration"); + } + + 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/infinispan/src/test/java/io/quarkus/ts/cache/infinispan/OpenShiftInfinispanCacheIT.java b/cache/infinispan/src/test/java/io/quarkus/ts/cache/infinispan/OpenShiftInfinispanCacheIT.java new file mode 100644 index 000000000..49e4e4a14 --- /dev/null +++ b/cache/infinispan/src/test/java/io/quarkus/ts/cache/infinispan/OpenShiftInfinispanCacheIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.infinispan; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftInfinispanCacheIT extends InfinispanCacheIT { +} diff --git a/cache/infinispan/src/test/resources/test.properties b/cache/infinispan/src/test/resources/test.properties new file mode 100644 index 000000000..4d3b274e8 --- /dev/null +++ b/cache/infinispan/src/test/resources/test.properties @@ -0,0 +1,3 @@ +ts.infinispan.openshift.use-internal-service-as-url=true +quarkus.infinispan-client.username=admin +quarkus.infinispan-client.password=password diff --git a/pom.xml b/pom.xml index b19afadce..3e279d67e 100644 --- a/pom.xml +++ b/pom.xml @@ -456,6 +456,7 @@ env-info cache/caffeine cache/redis + cache/infinispan infinispan-client