From e37da24bab8cb16b5134c08626400aefbe4b7655 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 2 Feb 2026 14:30:09 +0100 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 95fb2956ff..fb65810359 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 4.1.0-SNAPSHOT + 4.1.x-GH-3290-SNAPSHOT Spring Data Redis Spring Data module for Redis From 7dff03325b17f8307230d1726ad9e7bbaf8f8c23 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 3 Feb 2026 11:44:18 +0100 Subject: [PATCH 2/2] hacking --- .../redis/cache/CacheWriterOperation.java | 27 ++++++++++ .../redis/cache/DefaultRedisCacheWriter.java | 13 ++++- .../data/redis/cache/RedisCacheManager.java | 39 +++++++++++++- .../data/redis/cache/RedisCacheWriter.java | 7 +++ .../redis/cache/ResetCachesStrategies.java | 51 +++++++++++++++++++ .../cache/RedisCacheManagerUnitTests.java | 22 +++++++- 6 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/cache/CacheWriterOperation.java create mode 100644 src/main/java/org/springframework/data/redis/cache/ResetCachesStrategies.java diff --git a/src/main/java/org/springframework/data/redis/cache/CacheWriterOperation.java b/src/main/java/org/springframework/data/redis/cache/CacheWriterOperation.java new file mode 100644 index 0000000000..d568a48df3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/cache/CacheWriterOperation.java @@ -0,0 +1,27 @@ +/* + * Copyright 2026-present the original author or 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 org.springframework.data.redis.cache; + +import org.jspecify.annotations.Nullable; + +/** + * @author Christoph Strobl + */ +interface CacheWriterOperation { + + @Nullable + T doWithCacheWriter(RedisCacheWriter cacheWriter); +} diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java index a8b70dbcb5..e02443fc97 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.cache; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -24,6 +25,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -535,10 +537,17 @@ Long doUnlock(String name, RedisConnection connection) { return connection.keyCommands().del(createCacheLockKey(name)); } - private T execute(String name, Function callback) { + @Override + public T execute(Function callback) { + return execute(null, callback); + } + + private T execute(@Nullable String name, Function callback) { try (RedisConnection connection = this.connectionFactory.getConnection()) { - checkAndPotentiallyWaitUntilUnlocked(name, connection); + if(StringUtils.hasText(name)) { + checkAndPotentiallyWaitUntilUnlocked(name, connection); + } return callback.apply(connection); } } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java index fb25d8ecef..81dc0f0794 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java @@ -27,6 +27,8 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; +import org.springframework.data.redis.cache.ResetCachesStrategies.DefaultResetStrategy; +import org.springframework.data.redis.cache.ResetCachesStrategies.ResetCachesStrategy; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.util.Assert; @@ -64,6 +66,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager private final Map initialCacheConfiguration; + private final ResetCachesStrategy resetCachesStrategy; + /** * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default * {@link RedisCacheConfiguration}. @@ -109,6 +113,19 @@ private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration this.cacheWriter = cacheWriter; this.initialCacheConfiguration = new LinkedHashMap<>(); this.allowRuntimeCacheCreation = allowRuntimeCacheCreation; + this.resetCachesStrategy = DefaultResetStrategy.INSTANCE; + } + + private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration redisCacheConfiguration, boolean allowRuntimeCacheCreation, Map initialCaches, ResetCachesStrategy resetCachesStrategy) { + + Assert.notNull(redisCacheConfiguration, "DefaultCacheConfiguration must not be null"); + Assert.notNull(cacheWriter, "CacheWriter must not be null"); + + this.defaultCacheConfiguration = redisCacheConfiguration; + this.cacheWriter = cacheWriter; + this.initialCacheConfiguration = new LinkedHashMap<>(initialCaches); + this.allowRuntimeCacheCreation = allowRuntimeCacheCreation; + this.resetCachesStrategy = resetCachesStrategy; } /** @@ -225,6 +242,8 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d this.initialCacheConfiguration.putAll(initialCacheConfigurations); } + + /** * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager}. * @@ -312,6 +331,16 @@ public boolean isAllowRuntimeCacheCreation() { return this.allowRuntimeCacheCreation; } + @Override + public void resetCaches() { + + if(resetCachesStrategy instanceof CacheWriterOperation operation) { + operation.doWithCacheWriter(cacheWriter); + return; + } + super.resetCaches(); + } + /** * Return an {@link Collections#unmodifiableMap(Map) unmodifiable Map} containing {@link String caches name} mapped to * the {@link RedisCache} {@link RedisCacheConfiguration configuration}. @@ -404,6 +433,7 @@ public static class RedisCacheManagerBuilder { private boolean allowRuntimeCacheCreation = true; private boolean enableTransactions; + private ResetCachesStrategy resetCachesStrategy = ResetCachesStrategies.oneByOne(); private CacheStatisticsCollector statisticsCollector = CacheStatisticsCollector.none(); @@ -610,6 +640,11 @@ public RedisCacheManagerBuilder withInitialCacheConfigurations( return this; } + public RedisCacheManagerBuilder withResetCachesStrategy(ResetCachesStrategy resetCachesStrategy) { + this.resetCachesStrategy = resetCachesStrategy; + return this; + } + /** * Get the {@link RedisCacheConfiguration} for a given cache by its name. * @@ -654,7 +689,9 @@ public RedisCacheManager build() { } private RedisCacheManager newRedisCacheManager(RedisCacheWriter cacheWriter) { - return new RedisCacheManager(cacheWriter, cacheDefaults(), this.allowRuntimeCacheCreation, this.initialCaches); + return new RedisCacheManager(cacheWriter, cacheDefaults(), this.allowRuntimeCacheCreation, this.initialCaches, this.resetCachesStrategy); } } + + } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java index 338b053e05..dae75f3aff 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -18,10 +18,13 @@ import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.util.Assert; /** @@ -342,6 +345,10 @@ default boolean invalidate(String name, byte[] pattern) { */ void clearStatistics(String name); + default T execute(Function callback) { + throw new UnsupportedOperationException("execute(...) is not supported by this RedisCacheWriter"); + } + /** * Obtain a {@link RedisCacheWriter} using the given {@link CacheStatisticsCollector} to collect metrics. * diff --git a/src/main/java/org/springframework/data/redis/cache/ResetCachesStrategies.java b/src/main/java/org/springframework/data/redis/cache/ResetCachesStrategies.java new file mode 100644 index 0000000000..af1ab8730b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/cache/ResetCachesStrategies.java @@ -0,0 +1,51 @@ +/* + * Copyright 2026-present the original author or 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 org.springframework.data.redis.cache; + +import org.springframework.data.redis.connection.RedisServerCommands.FlushOption; + +/** + * @author Christoph Strobl + */ +public abstract class ResetCachesStrategies { + + public interface ResetCachesStrategy {} + + public static ResetCachesStrategy oneByOne() { + return DefaultResetStrategy.INSTANCE; + } + + public static ResetCachesStrategy flushing() { + return FlushingResetStrategy.INSTANCE; + } + + enum FlushingResetStrategy implements ResetCachesStrategy, CacheWriterOperation { + + INSTANCE; + + @Override + public String doWithCacheWriter(RedisCacheWriter cacheWriter) { + return cacheWriter.execute(connection -> { + connection.serverCommands().flushDb(FlushOption.ASYNC); + return "ok"; + }); + } + } + + enum DefaultResetStrategy implements ResetCachesStrategy { + INSTANCE; + } +} diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java index 4d314b6540..f74c5ec7fb 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java @@ -20,15 +20,18 @@ import java.time.Duration; import java.util.Collections; +import java.util.function.Function; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.cache.Cache; import org.springframework.cache.transaction.TransactionAwareCacheDecorator; import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisServerCommands; import org.springframework.test.util.ReflectionTestUtils; /** @@ -211,4 +214,21 @@ void customizeRedisCacheConfigurationBasedOnDefaultsIsImmutable() { assertThat(defaultCacheConfiguration.usePrefix()).isTrue(); assertThat(defaultCacheConfiguration.getTtlFunction().getTimeToLive(null, null)).isEqualTo(Duration.ofMinutes(30)); } + + @Test // GH-3290 + void configurationAllowsToSetResetCachesConfiguration() { + + RedisConnection connectionMock = mock(RedisConnection.class); + RedisServerCommands serverCommandsMock = mock(RedisServerCommands.class); + ArgumentCaptor> capture = ArgumentCaptor.captor(); + when(cacheWriter.execute(capture.capture())).thenReturn("ok"); + when(connectionMock.serverCommands()).thenReturn(serverCommandsMock); + + RedisCacheManager cacheManager = RedisCacheManager.builder(cacheWriter) + .withResetCachesStrategy(ResetCachesStrategies.flushing()).build(); + cacheManager.resetCaches(); + + capture.getValue().apply(connectionMock); + verify(serverCommandsMock).flushDb(any()); + } }