From 3872a7de96e942e9a98d04a716ac0a8f8b54e37c Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Fri, 24 Jul 2026 01:06:04 +0300 Subject: [PATCH] Added cache enablement config with disabled no-op behavior Added `enabled` cache configuration for Caffeine and Redis caches with a default-enabled compatibility path. When disabled, cache reads always miss, writes and invalidations become no-ops, and `computeIfAbsent` returns loader results without storing them. --- .../annotation/processor/CacheRunner.java | 2 ++ .../cache/caffeine/AbstractCaffeineCache.java | 34 +++++++++++++++++++ .../cache/caffeine/CaffeineCacheConfig.java | 4 +++ .../cache/caffeine/CacheRunner.java | 9 ++++- .../cache/caffeine/SyncCacheTests.java | 27 +++++++++++++++ .../cache/redis/AbstractRedisCache.java | 30 ++++++++++++++++ .../cache/redis/RedisCacheConfig.java | 4 +++ .../cache/redis/CacheRunner.java | 21 +++++++++++- .../cache/redis/SyncCacheTests.java | 27 +++++++++++++++ .../cache/symbol/processor/CacheRunner.kt | 1 + 10 files changed, 157 insertions(+), 2 deletions(-) diff --git a/cache/cache-annotation-processor/src/test/java/io/koraframework/cache/annotation/processor/CacheRunner.java b/cache/cache-annotation-processor/src/test/java/io/koraframework/cache/annotation/processor/CacheRunner.java index 7376bda99..bf7b6a5be 100644 --- a/cache/cache-annotation-processor/src/test/java/io/koraframework/cache/annotation/processor/CacheRunner.java +++ b/cache/cache-annotation-processor/src/test/java/io/koraframework/cache/annotation/processor/CacheRunner.java @@ -26,6 +26,7 @@ public static CaffeineCacheConfig getCaffeineConfig() { var config = Mockito.mock(CaffeineCacheConfig.class); var telemetry = Mockito.mock(CaffeineCacheTelemetryConfig.class); when(config.telemetry()).thenReturn(telemetry); + when(config.enabled()).thenReturn(true); when(config.maximumSize()).thenReturn(100_000L); when(config.expireAfterAccess()).thenReturn(null); when(config.expireAfterWrite()).thenReturn(null); @@ -37,6 +38,7 @@ public static CaffeineCacheConfig getCaffeineConfig() { public static RedisCacheConfig getRedisConfig() { var config = Mockito.mock(RedisCacheConfig.class); + when(config.enabled()).thenReturn(true); when(config.keyPrefix()).thenReturn("pref"); when(config.telemetry()).thenReturn(new $RedisCacheTelemetryConfig_ConfigValueMapper.RedisCacheTelemetryConfig_Impl( new $RedisCacheTelemetryConfig_RedisCacheLoggingConfig_ConfigValueMapper.RedisCacheLoggingConfig_Defaults(), diff --git a/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/AbstractCaffeineCache.java b/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/AbstractCaffeineCache.java index 6c5865369..7c3f224cd 100644 --- a/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/AbstractCaffeineCache.java +++ b/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/AbstractCaffeineCache.java @@ -20,6 +20,7 @@ public abstract class AbstractCaffeineCache implements CaffeineCache private final Cache caffeine; private final CaffeineCacheTelemetry telemetry; + private final boolean enabled; protected AbstractCaffeineCache(String cacheConfigPath, CaffeineCacheConfig config, @@ -27,6 +28,7 @@ protected AbstractCaffeineCache(String cacheConfigPath, CaffeineCacheTelemetryFactory telemetryFactory) { this.caffeine = factory.build(cacheConfigPath, config); this.telemetry = telemetryFactory.get(cacheConfigPath, getClass(), config.telemetry()); + this.enabled = config.enabled(); } @Override @@ -35,6 +37,9 @@ public V get(K key) { if (key == null) { return null; } + if (!enabled) { + return null; + } var observation = this.telemetry.observe(GET); observation.observeKey(key); @@ -55,6 +60,9 @@ public Map get(Collection keys) { if (keys == null || keys.isEmpty()) { return Collections.emptyMap(); } + if (!enabled) { + return Collections.emptyMap(); + } var observation = this.telemetry.observe(GET_MANY); observation.observeKeys(keys); @@ -72,6 +80,10 @@ public Map get(Collection keys) { @Override public Map getAll() { + if (!enabled) { + return Collections.emptyMap(); + } + var observation = this.telemetry.observe(GET_ALL); try { var values = Collections.unmodifiableMap(caffeine.asMap()); @@ -90,6 +102,9 @@ public V computeIfAbsent(K key, Function mappingFunction) { if (key == null) { return mappingFunction.apply(key); } + if (!enabled) { + return mappingFunction.apply(key); + } var observation = this.telemetry.observe(COMPUTE_IF_ABSENT); observation.observeKey(key); @@ -111,6 +126,9 @@ public Map computeIfAbsent(Collection keys, Function, Map> if (keys == null || keys.isEmpty()) { return mappingFunction.apply(Collections.emptySet()); } + if (!enabled) { + return mappingFunction.apply(Set.copyOf(keys)); + } var observation = this.telemetry.observe(COMPUTE_IF_ABSENT_MANY); observation.observeKeys(keys); @@ -133,6 +151,9 @@ public V put(K key, V value) { if (key == null || value == null) { return value; } + if (!enabled) { + return value; + } var observation = this.telemetry.observe(PUT); observation.observeKey(key); @@ -153,6 +174,9 @@ public Map put(Map keyAndValues) { if (keyAndValues == null || keyAndValues.isEmpty()) { return Collections.emptyMap(); } + if (!enabled) { + return keyAndValues; + } var observation = this.telemetry.observe(PUT_MANY); observation.observeKeys(keyAndValues.keySet()); @@ -173,6 +197,9 @@ public void invalidate(K key) { if (key == null) { return; } + if (!enabled) { + return; + } var observation = this.telemetry.observe(INVALIDATE); observation.observeKey(key); @@ -191,6 +218,9 @@ public void invalidate(Collection keys) { if (keys == null || keys.isEmpty()) { return; } + if (!enabled) { + return; + } var observation = this.telemetry.observe(INVALIDATE_MANY); observation.observeKeys(keys); @@ -206,6 +236,10 @@ public void invalidate(Collection keys) { @Override public void invalidateAll() { + if (!enabled) { + return; + } + var observation = this.telemetry.observe(INVALIDATE_ALL); try { observation.observeKeys(List.of()); diff --git a/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/CaffeineCacheConfig.java b/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/CaffeineCacheConfig.java index fa49f7415..f679a43f6 100644 --- a/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/CaffeineCacheConfig.java +++ b/cache/cache-caffeine/src/main/java/io/koraframework/cache/caffeine/CaffeineCacheConfig.java @@ -10,6 +10,10 @@ @ConfigMapper public interface CaffeineCacheConfig { + default boolean enabled() { + return true; + } + @Nullable Duration expireAfterWrite(); diff --git a/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/CacheRunner.java b/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/CacheRunner.java index a08b72a46..6649305d9 100644 --- a/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/CacheRunner.java +++ b/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/CacheRunner.java @@ -15,6 +15,7 @@ public static CaffeineCacheConfig getConfig() { var config = Mockito.mock(CaffeineCacheConfig.class); var telemetry = Mockito.mock(CaffeineCacheTelemetryConfig.class); when(config.telemetry()).thenReturn(telemetry); + when(config.enabled()).thenReturn(true); when(config.maximumSize()).thenReturn(100_000L); when(config.expireAfterAccess()).thenReturn(null); when(config.expireAfterWrite()).thenReturn(null); @@ -24,8 +25,14 @@ public static CaffeineCacheConfig getConfig() { return config; } protected DummyCache createCache() { + return createCache(true); + } + + protected DummyCache createCache(boolean enabled) { try { - return new DummyCache(getConfig(), caffeineCacheFactory(null), defaultCaffeineCacheTelemetryFactory(null, null, null, null)); + var config = getConfig(); + when(config.enabled()).thenReturn(enabled); + return new DummyCache(config, caffeineCacheFactory(null), defaultCaffeineCacheTelemetryFactory(null, null, null, null)); } catch (Exception e) { throw new IllegalStateException(e); } diff --git a/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/SyncCacheTests.java b/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/SyncCacheTests.java index c707d64b2..3fe62d09a 100644 --- a/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/SyncCacheTests.java +++ b/cache/cache-caffeine/src/test/java/io/koraframework/cache/caffeine/SyncCacheTests.java @@ -5,6 +5,10 @@ import org.junit.jupiter.api.TestInstance; import io.koraframework.cache.caffeine.testdata.DummyCache; +import java.util.List; +import java.util.Map; +import java.util.Set; + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class SyncCacheTests extends CacheRunner { @@ -81,4 +85,27 @@ void getFromCacheWhenCacheInvalidateAll() { final String fromCache = cache.get(key); assertNull(fromCache); } + + @Test + void operationsAreDisabledWhenConfigDisabled() { + // given + var disabledCache = createCache(false); + + // when + assertEquals("1", disabledCache.put("1", "1")); + assertEquals(Map.of("2", "2"), disabledCache.put(Map.of("2", "2"))); + + // then + assertNull(disabledCache.get("1")); + assertTrue(disabledCache.get(List.of("1", "2")).isEmpty()); + assertTrue(disabledCache.getAll().isEmpty()); + + // when + assertEquals("3", disabledCache.computeIfAbsent("3", k -> "3")); + assertEquals(Map.of("4", "4"), disabledCache.computeIfAbsent(Set.of("4"), keys -> Map.of("4", "4"))); + + // then + assertNull(disabledCache.get("3")); + assertTrue(disabledCache.get(List.of("4")).isEmpty()); + } } diff --git a/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/AbstractRedisCache.java b/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/AbstractRedisCache.java index 9a237eaf4..1cf8b4009 100644 --- a/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/AbstractRedisCache.java +++ b/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/AbstractRedisCache.java @@ -27,6 +27,7 @@ public abstract class AbstractRedisCache implements RedisCache { private final RedisCacheClient redisClient; private final RedisCacheTelemetry telemetry; + private final boolean enabled; private final RedisCacheKeyMapper keyMapper; private final RedisCacheValueMapper valueMapper; @@ -45,6 +46,7 @@ protected AbstractRedisCache(String cacheConfigPath, RedisCacheValueMapper valueMapper) { this.redisClient = redisClient; this.telemetry = telemetryFactory.get(cacheConfigPath, getClass(), config.telemetry()); + this.enabled = config.enabled(); this.keyMapper = keyMapper; this.valueMapper = valueMapper; this.expireAfterAccessMillis = (config.expireAfterAccess() == null) @@ -74,6 +76,9 @@ public V get(K key) { if (key == null) { return null; } + if (!enabled) { + return null; + } var observation = this.telemetry.observe(Operation.GET); return ScopedValue @@ -107,6 +112,9 @@ public Map get(Collection keys) { if (keys == null || keys.isEmpty()) { return Collections.emptyMap(); } + if (!enabled) { + return Collections.emptyMap(); + } var observation = this.telemetry.observe(Operation.GET_MANY); return ScopedValue @@ -152,6 +160,9 @@ public V put(K key, V value) { if (key == null || value == null) { return null; } + if (!enabled) { + return value; + } var observation = this.telemetry.observe(Operation.PUT); return ScopedValue @@ -186,6 +197,9 @@ public Map put(Map keyAndValues) { if (keyAndValues == null || keyAndValues.isEmpty()) { return Collections.emptyMap(); } + if (!enabled) { + return keyAndValues; + } var observation = this.telemetry.observe(Operation.PUT_MANY); return ScopedValue @@ -233,6 +247,9 @@ public V computeIfAbsent(K key, Function mappingFunction) { if (key == null) { return mappingFunction.apply(key); } + if (!enabled) { + return mappingFunction.apply(key); + } var observation = this.telemetry.observe(Operation.COMPUTE_IF_ABSENT); return ScopedValue @@ -290,6 +307,9 @@ public Map computeIfAbsent(Collection keys, Function, Map> if (keys == null || keys.isEmpty()) { return mappingFunction.apply(Collections.emptySet()); } + if (!enabled) { + return mappingFunction.apply(Set.copyOf(keys)); + } var observation = this.telemetry.observe(Operation.COMPUTE_IF_ABSENT_MANY); return ScopedValue @@ -370,6 +390,9 @@ public void invalidate(K key) { if (key == null) { return; } + if (!enabled) { + return; + } var observation = this.telemetry.observe(Operation.INVALIDATE); ScopedValue @@ -395,6 +418,9 @@ public void invalidate(Collection keys) { if (keys == null || keys.isEmpty()) { return; } + if (!enabled) { + return; + } var observation = this.telemetry.observe(Operation.INVALIDATE_MANY); ScopedValue @@ -420,6 +446,10 @@ public void invalidate(Collection keys) { @Override public void invalidateAll() { + if (!enabled) { + return; + } + var observation = this.telemetry.observe(Operation.INVALIDATE_ALL); ScopedValue .where(Observation.VALUE, observation) diff --git a/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/RedisCacheConfig.java b/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/RedisCacheConfig.java index ad2fb5cbd..b7eeff335 100644 --- a/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/RedisCacheConfig.java +++ b/cache/cache-redis-common/src/main/java/io/koraframework/cache/redis/RedisCacheConfig.java @@ -10,6 +10,10 @@ @ConfigMapper public interface RedisCacheConfig { + default boolean enabled() { + return true; + } + /** * Key prefix allow to avoid key collision in single Redis database between multiple caches * diff --git a/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/CacheRunner.java b/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/CacheRunner.java index c239a00c9..0669a3617 100644 --- a/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/CacheRunner.java +++ b/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/CacheRunner.java @@ -21,8 +21,19 @@ public abstract class CacheRunner extends Assertions implements LettuceRedisCach public static RedisCacheConfig getConfig(@Nullable Duration expireWrite, @Nullable Duration expireRead) { + return getConfig(expireWrite, expireRead, true); + } + + public static RedisCacheConfig getConfig(@Nullable Duration expireWrite, + @Nullable Duration expireRead, + boolean enabled) { return new RedisCacheConfig() { + @Override + public boolean enabled() { + return enabled; + } + @Override public String keyPrefix() { return PREFIX; @@ -96,8 +107,12 @@ public LettuceTelemetryConfig telemetry() { } private DummyCache createDummyCache(RedisParams redisParams, Duration expireWrite, Duration expireRead) throws Exception { + return createDummyCache(redisParams, expireWrite, expireRead, true); + } + + private DummyCache createDummyCache(RedisParams redisParams, Duration expireWrite, Duration expireRead, boolean enabled) throws Exception { var lettuceClient = createLettuce(redisParams); - return new DummyCache(getConfig(expireWrite, expireRead), lettuceClient, defaultRedisCacheTelemetryFactory(null, null, null, null), + return new DummyCache(getConfig(expireWrite, expireRead, enabled), lettuceClient, defaultRedisCacheTelemetryFactory(null, null, null, null), stringRedisCacheKeyMapper(), stringRedisCacheValueMapper()); } @@ -112,4 +127,8 @@ protected DummyCache createCacheExpireWrite(RedisParams redisParams, Duration ex protected DummyCache createCacheExpireRead(RedisParams redisParams, Duration expireRead) throws Exception { return createDummyCache(redisParams, null, expireRead); } + + protected DummyCache createCacheDisabled(RedisParams redisParams) throws Exception { + return createDummyCache(redisParams, null, null, false); + } } diff --git a/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/SyncCacheTests.java b/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/SyncCacheTests.java index 99df88018..4397e2341 100644 --- a/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/SyncCacheTests.java +++ b/cache/cache-redis-lettuce/src/test/java/io/koraframework/cache/redis/SyncCacheTests.java @@ -4,8 +4,13 @@ import io.koraframework.test.redis.RedisTestContainer; import io.lettuce.core.FlushMode; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import java.util.List; +import java.util.Map; +import java.util.Set; + @TestInstance(TestInstance.Lifecycle.PER_CLASS) @RedisTestContainer class SyncCacheTests extends AbstractSyncCacheTests { @@ -18,4 +23,26 @@ void setup(RedisParams redisParams) throws Exception { cache = createCache(redisParams); } } + + @Test + void operationsAreDisabledWhenConfigDisabled() throws Exception { + // given + var disabledCache = createCacheDisabled(redisParams); + + // when + assertEquals("1", disabledCache.put("1", "1")); + assertEquals(Map.of("2", "2"), disabledCache.put(Map.of("2", "2"))); + + // then + assertNull(disabledCache.get("1")); + assertTrue(disabledCache.get(List.of("1", "2")).isEmpty()); + + // when + assertEquals("3", disabledCache.computeIfAbsent("3", k -> "3")); + assertEquals(Map.of("4", "4"), disabledCache.computeIfAbsent(Set.of("4"), keys -> Map.of("4", "4"))); + + // then + assertNull(disabledCache.get("3")); + assertTrue(disabledCache.get(List.of("4")).isEmpty()); + } } diff --git a/cache/cache-symbol-processor/src/test/kotlin/io/koraframework/cache/symbol/processor/CacheRunner.kt b/cache/cache-symbol-processor/src/test/kotlin/io/koraframework/cache/symbol/processor/CacheRunner.kt index 40a3d7e21..65ba621b1 100644 --- a/cache/cache-symbol-processor/src/test/kotlin/io/koraframework/cache/symbol/processor/CacheRunner.kt +++ b/cache/cache-symbol-processor/src/test/kotlin/io/koraframework/cache/symbol/processor/CacheRunner.kt @@ -28,6 +28,7 @@ class CacheRunner { val config = Mockito.mock(CaffeineCacheConfig::class.java) val telemetry = Mockito.mock(CaffeineCacheTelemetryConfig::class.java) `when`(config.telemetry()).thenReturn(telemetry) + `when`(config.enabled()).thenReturn(true) `when`(config.maximumSize()).thenReturn(100000L) `when`(config.expireAfterAccess()).thenReturn(null) `when`(config.expireAfterWrite()).thenReturn(null)