diff --git a/caching/pom.xml b/caching/pom.xml index 730689133eabc128e03fcff2b4fe2ce550f07bea..35cb8aa7827c25f0686647d31be47029ebf088e2 100644 --- a/caching/pom.xml +++ b/caching/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <dependencies> diff --git a/caching/src/main/java/org/eclipsefoundation/caching/config/EclipseCacheConfig.java b/caching/src/main/java/org/eclipsefoundation/caching/config/EclipseCacheConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..17554c1457866ae91b14a232048edde5bf3e0f67 --- /dev/null +++ b/caching/src/main/java/org/eclipsefoundation/caching/config/EclipseCacheConfig.java @@ -0,0 +1,36 @@ +/********************************************************************* +* Copyright (c) 2025 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.caching.config; + +import java.util.List; +import java.util.Optional; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +/** + * + */ +@ConfigMapping(prefix = "eclipse.cache") +public interface EclipseCacheConfig { + + @WithDefault("900") + long ttlMaxSeconds(); + + @WithDefault("") + Optional<List<Class<?>>> cacheBypassClasses(); + + CacheResource resource(); + + public interface CacheResource { + @WithDefault("true") + boolean enabled(); + } +} diff --git a/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java b/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java index 903358007dbf8e160c29033dd53b3f7f7fc03a31..4f752720f000c6dc93f09a5c4ede65a2b0e12627 100644 --- a/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java +++ b/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java @@ -12,6 +12,7 @@ package org.eclipsefoundation.caching.service.impl; import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -20,14 +21,13 @@ import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; -import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.caching.config.CacheKeyClassTagResolver; +import org.eclipsefoundation.caching.config.EclipseCacheConfig; import org.eclipsefoundation.caching.exception.CacheCalculationException; import org.eclipsefoundation.caching.model.CacheWrapper; import org.eclipsefoundation.caching.model.CacheWrapperBuilder; import org.eclipsefoundation.caching.model.ParameterizedCacheKey; import org.eclipsefoundation.caching.model.ParameterizedCacheKeyBuilder; -import org.eclipsefoundation.caching.namespaces.CachingPropertyNames; import org.eclipsefoundation.caching.service.CachingService; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; @@ -58,22 +58,28 @@ import jakarta.ws.rs.core.MultivaluedMap; public class QuarkusCachingService implements CachingService { private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusCachingService.class); - private final long ttlWrite; + private final EclipseCacheConfig config; private final RequestWrapper wrapper; private final Cache cache; - public QuarkusCachingService(RequestWrapper wrapper, @CacheName("default") Cache cache, - @ConfigProperty(name = CachingPropertyNames.CACHE_TTL_MAX_SECONDS, defaultValue = "900") long ttlWrite) { + public QuarkusCachingService(RequestWrapper wrapper, @CacheName("default") Cache cache, EclipseCacheConfig config) { this.cache = cache; this.wrapper = wrapper; - this.ttlWrite = ttlWrite; + this.config = config; } @Override public <T> CacheWrapper<T> get(String id, MultivaluedMap<String, String> params, Class<?> rawType, Callable<? extends T> callable) { Objects.requireNonNull(callable); + // create the cache key for the entry, cloning the map for key integrity ParameterizedCacheKey cacheKey = ParameterizedCacheKeyBuilder.builder().clazz(rawType).id(id).params(cloneMap(params)).build(); + // allow for the cache to be bypassed for configured types + Optional<List<Class<?>>> bypasses = config.cacheBypassClasses(); + if (bypasses.isPresent() && bypasses.get().stream().anyMatch(clazz -> clazz.equals(rawType))) { + return getUncachedValue(cacheKey, callable); + } + // fetch the in-memory cached version of the data LOGGER.debug("Retrieving cache value for '{}'", cacheKey); return get(cacheKey, callable); } @@ -146,7 +152,7 @@ public class QuarkusCachingService implements CachingService { @Override public long getMaxAge() { - return TimeUnit.MILLISECONDS.convert(ttlWrite, TimeUnit.SECONDS); + return TimeUnit.MILLISECONDS.convert(config.ttlMaxSeconds(), TimeUnit.SECONDS); } @Override @@ -175,17 +181,11 @@ public class QuarkusCachingService implements CachingService { @CacheResult(cacheName = "default") <T> CacheWrapper<T> get(@MeterTag(resolver = CacheKeyClassTagResolver.class) @CacheKey ParameterizedCacheKey cacheKey, Callable<? extends T> callable) { - - CacheWrapperBuilder<T> cacheWrap = CacheWrapperBuilder.builder(); - try { - T data = callable.call(); - cacheWrap.data(Optional.ofNullable(data)); - } catch (Exception e) { - LOGGER.error("Error while creating cache entry for key '{}': \n", cacheKey, e); - cacheWrap.errorType(Optional.of(e.getClass())); - } + // fetch the wrapped cache value for the callable + CacheWrapper<T> cacheWrap = getUncachedValue(cacheKey, callable); + // set an expiration entry for the item getExpiration(false, cacheKey); - return cacheWrap.build(); + return cacheWrap; } @CacheResult(cacheName = "ttl") @@ -197,6 +197,26 @@ public class QuarkusCachingService implements CachingService { return System.currentTimeMillis() + getMaxAge(); } + /** + * Using the passed callable and cache key, fetch an uncached value for the callable and return it. + * + * @param <T> the inner datatype of the cache result + * @param cacheKey cache key for the would-be entry + * @param callable function that produces the results on call + * @return cache wrapper containing the data if possible, and the error if there was a problem fetching the data + */ + private <T> CacheWrapper<T> getUncachedValue(ParameterizedCacheKey cacheKey, Callable<? extends T> callable) { + CacheWrapperBuilder<T> cacheWrap = CacheWrapperBuilder.builder(); + try { + T data = callable.call(); + cacheWrap.data(Optional.ofNullable(data)); + } catch (Exception e) { + LOGGER.error("Error while creating cache entry for key '{}': \n", cacheKey, e); + cacheWrap.errorType(Optional.of(e.getClass())); + } + return cacheWrap.build(); + } + /** * To prevent modification of maps/cache key entries post retrieval, we should be referencing copies of the maps rather than the direct * passed reference for safety. diff --git a/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java b/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java index 04787fd699574669db61a3265c1e491ceac93500..bdf2ec204d988a6271f95b16119cdae84757f276 100644 --- a/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java +++ b/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java @@ -18,6 +18,7 @@ import org.eclipsefoundation.caching.model.CacheWrapper; import org.eclipsefoundation.caching.model.ParameterizedCacheKey; import org.eclipsefoundation.caching.model.ParameterizedCacheKeyBuilder; import org.eclipsefoundation.caching.service.CachingService; +import org.eclipsefoundation.caching.test.models.SampleUncachedData; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -40,7 +41,7 @@ class DefaultQuarkusCachingServiceTest { private static final String PARAMETER_NAME = "id"; private static final String SECOND_PARAMETER_NAME = "ids"; - + @Inject @CacheName("default") Cache cache; @@ -72,6 +73,26 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertEquals(cacheValue, value.data().get(), "Expected input and output value to be the same"); } + @Test + void get_success_repeatCallsReturnCachedData() { + // generate a random key + String id = UUID.randomUUID().toString(); + String cacheValue = "sample"; + // check that we have no match for ID before proceeding + Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); + // do the cache call + CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> "sample"); + // call again with a different result for callable (should return cached value here) + CacheWrapper<String> repeatedValue = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> "sample 2"); + + Assertions + .assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), + "Expected new cache value to have a key present in cache key set"); + Assertions.assertTrue(value.data().isPresent(), "Cache value should have been present but was missing"); + Assertions.assertEquals(cacheValue, value.data().get(), "Expected input and output value to be the same"); + Assertions.assertEquals(cacheValue, repeatedValue.data().get(), "Expected input and repeated cache call value to be the same"); + } + @Test void get_success_generatesTtlEntry() { // generate a random key @@ -165,6 +186,28 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertTrue(value2.data().isPresent(), "String cache value should have been present but was missing"); } + @Test + void get_success_canBypassCache() { + // generate a random key + String id = UUID.randomUUID().toString(); + // values will differ in the actual function, representing a changing downstream source + CacheWrapper<SampleUncachedData> value = svc.get(id, null, SampleUncachedData.class, () -> new SampleUncachedData("value 1")); + CacheWrapper<SampleUncachedData> repeatedCallValue = svc + .get(id, null, SampleUncachedData.class, () -> new SampleUncachedData("value 2")); + + // values should differ as the callable has different results and cache is bypassed + Assertions + .assertNotEquals(value, repeatedCallValue, + "Data from distinct cache calls should not be the same for bypassed class results"); + // there should be no cache entries if cache was bypassed + Assertions + .assertFalse(lookupCacheKeyUsingRawCache(id, Optional.empty(), Optional.of(SampleUncachedData.class), cache), + "Expected no cache value for key of bypassed class types"); + Assertions + .assertFalse(lookupCacheKeyUsingRawCache(id, Optional.empty(), Optional.of(SampleUncachedData.class), ttlCache), + "Expected no TTL cache value for key of bypassed class types"); + } + @Test void get_failure_errorThrown() { String id = UUID.randomUUID().toString(); @@ -174,9 +217,12 @@ class DefaultQuarkusCachingServiceTest { // value should be null as there was an error CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> { throw new RuntimeException("Kaboom"); }); - Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); + Assertions + .assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.data().isEmpty(), "Cache value should have been missing but was present"); - Assertions.assertEquals(RuntimeException.class, value.errorType().get(), "The exception type caught in the wrapper should be the same as thrown"); + Assertions + .assertEquals(RuntimeException.class, value.errorType().get(), + "The exception type caught in the wrapper should be the same as thrown"); } @Test @@ -185,10 +231,12 @@ class DefaultQuarkusCachingServiceTest { // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); - + // value should be null as there was an error, but a TTL should be set svc.get(id, new MultivaluedHashMap<>(), String.class, () -> { throw new RuntimeException("Kaboom"); }); - Assertions.assertTrue(lookupCacheKeyUsingRawCache(id, Optional.empty(), ttlCache), "Expected random UUID key to have a TTL value, but was empty"); + Assertions + .assertTrue(lookupCacheKeyUsingRawCache(id, Optional.empty(), ttlCache), + "Expected random UUID key to have a TTL value, but was empty"); } @Test @@ -201,7 +249,8 @@ class DefaultQuarkusCachingServiceTest { // value should be empty as null gets interpreted as an empty optional CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> null); - Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); + Assertions + .assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.data().isEmpty(), "Cache value should have been missing but was present"); Assertions.assertTrue(value.errorType().isEmpty(), "There should be no exception caught in this case"); } @@ -215,7 +264,8 @@ class DefaultQuarkusCachingServiceTest { // value should be null as there was an error CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> null); - Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); + Assertions + .assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.data().isEmpty(), "Cache value should have been missing but was present"); Assertions.assertTrue(value.errorType().isEmpty(), "There should be no exception caught in this case"); } @@ -548,8 +598,8 @@ class DefaultQuarkusCachingServiceTest { } /** - * Using similar logic to whats in the key set retrieval logic in the caching service, iterate over keys and look for - * matches for the passed parameters. + * Using similar logic to whats in the key set retrieval logic in the caching service, iterate over keys and look for matches for the + * passed parameters. * * @param id the ID to lookup in cache keys * @param params optional map of params to match on @@ -561,8 +611,8 @@ class DefaultQuarkusCachingServiceTest { } /** - * Using similar logic to whats in the key set retrieval logic in the caching service, iterate over keys and look for - * matches for the passed parameters. + * Using similar logic to whats in the key set retrieval logic in the caching service, iterate over keys and look for matches for the + * passed parameters. * * @param id the ID to lookup in cache keys * @param params optional map of params to match on diff --git a/caching/src/test/java/org/eclipsefoundation/caching/test/models/SampleUncachedData.java b/caching/src/test/java/org/eclipsefoundation/caching/test/models/SampleUncachedData.java new file mode 100644 index 0000000000000000000000000000000000000000..463d7cdea572a067883156c30530c72b90a87c9a --- /dev/null +++ b/caching/src/test/java/org/eclipsefoundation/caching/test/models/SampleUncachedData.java @@ -0,0 +1,17 @@ +/********************************************************************* +* Copyright (c) 2025 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.caching.test.models; + +/** + * Used in validating the cache bypass configuration. + */ +public record SampleUncachedData(String value) { + +} diff --git a/caching/src/test/resources/application.properties b/caching/src/test/resources/application.properties index 1bf869f7d2774faa1b21035f9de2ead0a28bd54e..f20dec4f3e80754b797cd164acd306b7a79faa9f 100644 --- a/caching/src/test/resources/application.properties +++ b/caching/src/test/resources/application.properties @@ -5,6 +5,8 @@ eclipse.cache.loading."sample".start-at-boot=true eclipse.cache.loading."sample".runtime-boot-key=true eclipse.cache.loading."sample".timeout=10 +eclipse.cache.cache-bypass-classes=org.eclipsefoundation.caching.test.models.SampleUncachedData + ## OAUTH CONFIG eclipse.security.oauth2.override.enabled=true diff --git a/core/pom.xml b/core/pom.xml index 601bca2735fc033f1533ac2f9c98809a3b48a65a..8bb87861ba7ad48ea558544cd8a61e26cd4ce490 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <properties> diff --git a/efservices/pom.xml b/efservices/pom.xml index 24341554e3f8c5845713f8b76c89f58e966b6963..4fcc9009a7744f58f12dc40caee3286a494ea476 100644 --- a/efservices/pom.xml +++ b/efservices/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <properties> diff --git a/http/pom.xml b/http/pom.xml index 4902fdebbd3c429e1aa7639578ab4015d58a790b..b0dad3db6fb9a19ad708691643e0bc61f29ec483 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <properties> diff --git a/persistence/deployment/pom.xml b/persistence/deployment/pom.xml index 4483bf20da7668f4a555a09a2bb3b0276a458dc9..dbb3a8fd86837f61f63074350074d304d5ccdcf7 100644 --- a/persistence/deployment/pom.xml +++ b/persistence/deployment/pom.xml @@ -8,7 +8,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-persistence-parent</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> </parent> <artifactId>quarkus-persistence-deployment</artifactId> <name>Persistence - Deployment</name> diff --git a/persistence/pom.xml b/persistence/pom.xml index 7ef7555788cb9d5b55a85eadbd07ca24ee06a71c..5e22dce9b8f58657a522bde9b0b405de8a7ee745 100644 --- a/persistence/pom.xml +++ b/persistence/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <modules> diff --git a/persistence/runtime/pom.xml b/persistence/runtime/pom.xml index 164db2726044fa049bc43e6757652d74b9ec3809..f5293c70428c9f20a45fac3247df88aa13b40825 100644 --- a/persistence/runtime/pom.xml +++ b/persistence/runtime/pom.xml @@ -4,7 +4,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-persistence-parent</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> </parent> <artifactId>quarkus-persistence</artifactId> <name>Persistence - Runtime</name> diff --git a/pom.xml b/pom.xml index 3f3e1c10607e6aa00cd1aca3c5b23e5e4eaff6b7..9404778d6370917e7bb076f930df33daa55df1d3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> <name>Java SDK Commons</name> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <packaging>pom</packaging> <properties> <compiler-plugin.version>3.13.0</compiler-plugin.version> diff --git a/testing/pom.xml b/testing/pom.xml index 56f33d88a2e2170aa024f6e1e74c68f774458e7c..8e3565e189e0f9402f74385a54b7d6efbd72b417 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <properties> diff --git a/utils/pom.xml b/utils/pom.xml index 5ad5b7f11c41f6c07a4b97a374e4df18ee4ce469..064d8e6639c76636efc8567158f567b9ef75deea 100644 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.2.3</version> + <version>1.2.4-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <dependencies>