diff --git a/gradle.properties b/gradle.properties index 1d08ac906..2ec0b51c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=6.7.0-SNAPSHOT +projectVersion=6.7.1-SNAPSHOT projectGroup=io.micronaut.redis title=Micronaut Redis diff --git a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/RedisSetting.java b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/RedisSetting.java index 0d8b21414..e16d84b19 100644 --- a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/RedisSetting.java +++ b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/RedisSetting.java @@ -46,6 +46,12 @@ public interface RedisSetting { * Default configuration for Redis caches. */ String REDIS_CACHE = PREFIX + ".cache"; + + /** + * Configuration for dynamic Redis caches. + */ + String REDIS_DYNAMIC_CACHE = REDIS_CACHE + ".dynamic"; + /** * Configured Redis caches. */ diff --git a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisCache.java b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisCache.java index bfc23d064..9c1320418 100644 --- a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisCache.java +++ b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisCache.java @@ -46,10 +46,11 @@ /** * An implementation of {@link SyncCache} for Lettuce / Redis. * - * @author Graeme Rocher + * @author Graeme Rocher, Ferdinand Armbruster * @since 1.0 */ @EachBean(RedisCacheConfiguration.class) +@Requires(classes = SyncCache.class, property = RedisSetting.PREFIX + ".enabled", defaultValue = StringUtils.TRUE, notEquals = StringUtils.FALSE) @Requires(classes = SyncCache.class, property = RedisSetting.REDIS_POOL + ".enabled", defaultValue = StringUtils.FALSE, notEquals = StringUtils.TRUE) public class RedisCache extends AbstractRedisCache> { private final RedisAsyncCache asyncCache; diff --git a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisConnectionPoolCache.java b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisConnectionPoolCache.java index 455db811e..be180a40f 100644 --- a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisConnectionPoolCache.java +++ b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/RedisConnectionPoolCache.java @@ -51,10 +51,11 @@ /** * An implementation of {@link SyncCache} for Lettuce / Redis using connection pooling. * - * @author Graeme Rocher, Kovalov Illia + * @author Graeme Rocher, Kovalov Illia, Ferdinand Armbruster * @since 5.3.0 */ @EachBean(RedisCacheConfiguration.class) +@Requires(classes = SyncCache.class, property = RedisSetting.PREFIX + ".enabled", defaultValue = StringUtils.TRUE, notEquals = StringUtils.FALSE) @Requires(classes = SyncCache.class, property = RedisSetting.REDIS_POOL + ".enabled", defaultValue = StringUtils.FALSE, notEquals = StringUtils.FALSE) public class RedisConnectionPoolCache extends AbstractRedisCache>> { private static final Logger LOG = LoggerFactory.getLogger(RedisConnectionPoolCache.class); diff --git a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/bypass/BypassDynamicCacheManager.java b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/bypass/BypassDynamicCacheManager.java new file mode 100644 index 000000000..9fb274361 --- /dev/null +++ b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/bypass/BypassDynamicCacheManager.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.configuration.lettuce.cache.bypass; + +import io.micronaut.cache.DynamicCacheManager; +import io.micronaut.cache.SyncCache; +import io.micronaut.configuration.lettuce.RedisSetting; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * An implementation of {@link DynamicCacheManager} to bypass the caching mechanism, if redis is deactivated. + * + * @author Armbruster Ferdinand + * @since 6.7.1 + */ +@Singleton +@Requires(classes = SyncCache.class, property = RedisSetting.PREFIX + ".enabled", defaultValue = StringUtils.TRUE, notEquals = StringUtils.TRUE) +public class BypassDynamicCacheManager implements DynamicCacheManager { + + private static final Logger LOG = LoggerFactory.getLogger(BypassDynamicCacheManager.class); + + /** + * Creates a new bypass cache for the given arguments. + * + * @param name The name of the dynamic cache + */ + @Override + public @NonNull SyncCache getCache(String name) { + if (LOG.isDebugEnabled()) { + LOG.debug("Create BypassCache for {}", name); + } + return new BypassCache(name); + } + + /** + * An implementation of {@link SyncCache} to bypass the caching mechanism. + * + * @author Armbruster Ferdinand + * @since 6.7.1 + */ + protected class BypassCache implements SyncCache { + + private final String name; + + public BypassCache(String name) { + this.name = name; + } + + @Override + public @NonNull Optional get(@NonNull Object key, @NonNull Argument requiredType) { + return Optional.empty(); + } + + @Override + public T get(@NonNull Object key, @NonNull Argument requiredType, @NonNull Supplier supplier) { + return null; + } + + @Override + public @NonNull Optional putIfAbsent(@NonNull Object key, @NonNull T value) { + return Optional.empty(); + } + + @Override + public void put(@NonNull Object key, @NonNull Object value) { } + + @Override + public void invalidate(@NonNull Object key) { } + + @Override + public void invalidateAll() { } + + @Override + public String getName() { + return name; + } + + @Override + public Object getNativeCache() { + return null; + } + } + +} + diff --git a/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/dynamic/RedisDynamicCacheManager.java b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/dynamic/RedisDynamicCacheManager.java new file mode 100644 index 000000000..817245ffe --- /dev/null +++ b/redis-lettuce/src/main/java/io/micronaut/configuration/lettuce/cache/dynamic/RedisDynamicCacheManager.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.configuration.lettuce.cache.dynamic; + +import io.lettuce.core.api.StatefulConnection; +import io.micronaut.cache.DynamicCacheManager; +import io.micronaut.cache.SyncCache; +import io.micronaut.configuration.lettuce.RedisSetting; +import io.micronaut.configuration.lettuce.cache.DefaultRedisCacheConfiguration; +import io.micronaut.configuration.lettuce.cache.RedisCache; +import io.micronaut.configuration.lettuce.cache.RedisCacheConfiguration; +import io.micronaut.configuration.lettuce.cache.bypass.BypassDynamicCacheManager; +import io.micronaut.context.BeanLocator; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.StringUtils; +import io.micronaut.runtime.ApplicationConfiguration; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * An implementation of {@link DynamicCacheManager} for to create caches without having a configuration entry. + * By using default {@link DefaultRedisCacheConfiguration}. + * + * @author Armbruster Ferdinand + * @since 6.7.1 + */ +@Singleton +@Requires(classes = SyncCache.class, property = RedisSetting.PREFIX + ".enabled", defaultValue = StringUtils.TRUE, notEquals = StringUtils.FALSE) +@Requires(classes = SyncCache.class, property = RedisSetting.REDIS_DYNAMIC_CACHE + ".enabled", defaultValue = StringUtils.FALSE, notEquals = StringUtils.FALSE) +public class RedisDynamicCacheManager implements DynamicCacheManager> { + + private static final Logger LOG = LoggerFactory.getLogger(BypassDynamicCacheManager.class); + + private ApplicationConfiguration applicationConfiguration; + private DefaultRedisCacheConfiguration defaultRedisCacheConfiguration; + private ConversionService conversionService; + private BeanLocator beanLocator; + + public RedisDynamicCacheManager( + ApplicationConfiguration applicationConfiguration, + DefaultRedisCacheConfiguration defaultRedisCacheConfiguration, + ConversionService conversionService, + BeanLocator beanLocator + ) { + this.applicationConfiguration = applicationConfiguration; + this.defaultRedisCacheConfiguration = defaultRedisCacheConfiguration; + this.conversionService = conversionService; + this.beanLocator = beanLocator; + } + + /** + * Creates a new dynamic redis cache for the given arguments. + * + * @param name The name of the dynamic cache + */ + @Override + public @NonNull SyncCache> getCache(String name) { + + if (LOG.isDebugEnabled()) { + LOG.debug("Create DynamicRedisCache for {}", name); + } + + return new RedisCache( + defaultRedisCacheConfiguration, + new RedisCacheConfiguration(name, applicationConfiguration), + conversionService, + beanLocator + ); + } +} + diff --git a/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisCacheSpec.groovy b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisCacheSpec.groovy index c74e9f6ea..f89dabad3 100644 --- a/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisCacheSpec.groovy +++ b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisCacheSpec.groovy @@ -8,6 +8,7 @@ import io.micronaut.configuration.lettuce.RedisSpec import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanLocator import io.micronaut.context.exceptions.ConfigurationException +import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.core.convert.ConversionService import io.micronaut.core.type.Argument import io.micronaut.inject.qualifiers.Qualifiers @@ -21,17 +22,31 @@ import java.nio.charset.Charset import java.util.concurrent.ExecutionException /** - * @author Graeme Rocher + * @author Graeme Rocher, Ferdinand Armbruster * @since 1.0 */ class RedisCacheSpec extends RedisSpec { - ApplicationContext createApplicationContext() { - ApplicationContext.run( + ApplicationContext createApplicationContext(Map options = [:]) { + ApplicationContext.run([ 'redis.port': RedisContainerUtils.getRedisPort(), 'redis.caches.test.enabled': 'true', 'redis.caches.test.invalidate-scan-count': 2 - ) + ] + options) + } + + void "test bean is not instantiated if redis is disabled"(){ + setup: + ApplicationContext applicationContext = createApplicationContext(['redis.enabled': false]) + + when: + applicationContext.getBean(RedisCache, Qualifiers.byName("test")) + + then: + thrown(NoSuchBeanException) + + cleanup: + applicationContext.stop() } void "test read/write object from redis sync cache"() { diff --git a/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisPoolCacheSpec.groovy b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisPoolCacheSpec.groovy index 5d7d9e7a7..1d473608a 100644 --- a/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisPoolCacheSpec.groovy +++ b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/RedisPoolCacheSpec.groovy @@ -17,7 +17,7 @@ import io.micronaut.runtime.ApplicationConfiguration import java.nio.charset.Charset /** - * @author Kovalov Illia + * @author Kovalov Illia, Ferdinand Armbruster */ class RedisPoolCacheSpec extends RedisSpec { @@ -29,6 +29,20 @@ class RedisPoolCacheSpec extends RedisSpec { ] + options).environments("test").eagerInitSingletons(eagerInit).start() } + void "test bean is not instantiated if redis is disabled"(){ + setup: + ApplicationContext applicationContext = createApplicationContext('redis.enabled': 'false') + + when: + applicationContext.getBean(RedisCache, Qualifiers.byName("test")) + + then: + thrown(NoSuchBeanException) + + cleanup: + applicationContext.stop() + } + void "can be disabled where initialization is #description"() { setup: ApplicationContext applicationContext = createApplicationContext('redis.pool.enabled': 'false', eager) diff --git a/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/bypass/BypassDynamicCacheManagerSpec.groovy b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/bypass/BypassDynamicCacheManagerSpec.groovy new file mode 100644 index 000000000..2db801594 --- /dev/null +++ b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/bypass/BypassDynamicCacheManagerSpec.groovy @@ -0,0 +1,45 @@ +package io.micronaut.configuration.lettuce.cache.bypass + + +import io.micronaut.configuration.lettuce.RedisSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.NoSuchBeanException + +/** + * @author Ferdinand Armbruster + * + * @since 6.7.1 + */ +class BypassDynamicCacheManagerSpec extends RedisSpec { + + + ApplicationContext createApplicationContext(Boolean enabled) { + ApplicationContext.run(["redis.enabled": enabled]) + } + + + void "test BypassDynamicCacheManager should not be available if redis is active"() { + setup: + ApplicationContext applicationContext = createApplicationContext(true) + + when: + applicationContext.getBean(BypassDynamicCacheManager) + + then: + thrown(NoSuchBeanException) + } + + void "test bypassCache"() { + setup: + ApplicationContext applicationContext = createApplicationContext(false) + + when: + BypassDynamicCacheManager bypassDynamicCacheManager = applicationContext.getBean(BypassDynamicCacheManager) + BypassDynamicCacheManager.BypassCache bypassCache = bypassDynamicCacheManager.getCache("test") + + then: + bypassDynamicCacheManager != null + bypassCache != null + } + +} diff --git a/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/dynamic/RedisDynamicCacheManagerSpec.groovy b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/dynamic/RedisDynamicCacheManagerSpec.groovy new file mode 100644 index 000000000..0a956eff7 --- /dev/null +++ b/redis-lettuce/src/test/groovy/io/micronaut/configuration/lettuce/cache/dynamic/RedisDynamicCacheManagerSpec.groovy @@ -0,0 +1,59 @@ +package io.micronaut.configuration.lettuce.cache.dynamic + +import io.lettuce.core.api.StatefulRedisConnection +import io.micronaut.configuration.lettuce.RedisSpec +import io.micronaut.configuration.lettuce.cache.RedisCache +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.redis.test.RedisContainerUtils + +/** + * @author Ferdinand Armbruster + * @since 6.7.1 + */ +class RedisDynamicCacheManagerSpec extends RedisSpec { + + ApplicationContext createApplicationContext(boolean redisEnabled = true, boolean dynamicEnabled = true) { + ApplicationContext.run([ + "redis.enabled" : redisEnabled, + 'redis.port' : RedisContainerUtils.getRedisPort(), + "redis.cache.dynamic.enabled": dynamicEnabled + ]) + } + + void "test RedisDynamicCacheManager should not be available if redis is deactivated"() { + setup: + ApplicationContext applicationContext = createApplicationContext(false, true) + + when: + applicationContext.getBean(RedisDynamicCacheManager) + + then: + thrown(NoSuchBeanException) + } + + void "test RedisDynamicCacheManager should not be available if dynamic is deactivated"() { + setup: + ApplicationContext applicationContext = createApplicationContext(true, false) + + when: + applicationContext.getBean(RedisDynamicCacheManager) + + then: + thrown(NoSuchBeanException) + } + + void "test creation of redisCache object for given name"() { + setup: + ApplicationContext applicationContext = createApplicationContext(true, true) + + when: + RedisDynamicCacheManager redisDynamicCacheManager = applicationContext.getBean(RedisDynamicCacheManager) + RedisCache redisCache = redisDynamicCacheManager.getCache("test") + + then: + redisCache != null + redisCache.getNativeCache() instanceof StatefulRedisConnection + } + +} diff --git a/src/main/docs/guide/cache.adoc b/src/main/docs/guide/cache.adoc index cbcf08793..f89eac87b 100644 --- a/src/main/docs/guide/cache.adoc +++ b/src/main/docs/guide/cache.adoc @@ -27,3 +27,20 @@ redis: - Expiration is based on result from an implementation of api:configuration.lettuce.cache.expiration.ExpirationAfterWritePolicy[] include::{includedir}configurationProperties/io.micronaut.configuration.lettuce.cache.RedisCacheConfiguration.adoc[] + +== Dynamic Caching + +To get more flexibility for Caches, you can enable dynamic caching. Therefore no specific cache configuration is needed and the caches will be created dynamically at runtime. +The DefaultRedisCacheConfiguration will be used to setup the cache. "Static"- and "Dynamic"-caches can be used in parallel. + +.DynamicCache Configuration Example +[configuration] +---- +redis: + uri: redis://localhost + cache: + dynamic: + enabled: true + expire-after-write: 1h + expiration-after-write-policy: +---- diff --git a/src/main/docs/guide/config.adoc b/src/main/docs/guide/config.adoc index 656b0b6d9..f50a7322e 100644 --- a/src/main/docs/guide/config.adoc +++ b/src/main/docs/guide/config.adoc @@ -80,7 +80,7 @@ include::{testsredis}/NamedCodecFactory.groovy[tags=namedCodec2, indent=0] When the `redis-lettuce` module is activated a api:configuration.lettuce.health.RedisHealthIndicator[] is activated resulting in the `/health` endpoint and https://docs.micronaut.io/latest/api/io/micronaut/health/CurrentHealthStatus.html[CurrentHealthStatus] interface resolving the health of the Redis connection or connections. -The health indicator is enabled by default. To disable the health endpoint, you can do so via the config. +The health indicator is enabled by default. To disable the health endpoint, you can do so via the config. [configuration] ---- redis: @@ -100,3 +100,6 @@ You can disable the creation of Redis connections using the `redis.enabled` sett redis: enabled: false ---- + +If redis is disabled, the caching functionality will also be disabled and bypassed. +