Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-3290-SNAPSHOT</version>

<name>Spring Data Redis</name>
<description>Spring Data module for Redis</description>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be a @FunctionalInterface.


@Nullable
T doWithCacheWriter(RedisCacheWriter cacheWriter);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -535,10 +537,17 @@ Long doUnlock(String name, RedisConnection connection) {
return connection.keyCommands().del(createCacheLockKey(name));
}

private <T> T execute(String name, Function<RedisConnection, T> callback) {
@Override
public <T> T execute(Function<RedisConnection, T> callback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sure this will clear up w/ javadocs once we get firm on the direction, but it is currently not clear that the newly added execute method does not do the potential wait for unlocked (i.e. checkAndPotentiallyWaitUntilUnlocked).

return execute(null, callback);
}

private <T> T execute(@Nullable String name, Function<RedisConnection, T> callback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just call executeLockFree instead of adding the @Nullable here.


try (RedisConnection connection = this.connectionFactory.getConnection()) {
checkAndPotentiallyWaitUntilUnlocked(name, connection);
if(StringUtils.hasText(name)) {
checkAndPotentiallyWaitUntilUnlocked(name, connection);
}
return callback.apply(connection);
}
}
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sure I am missing something, but it seems that we are overlapping w/ current cache cleaning impl in BatchStrategy (keys and scan). Seems like we could just add another strategy for flushAsync().

Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -64,6 +66,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager

private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;

private final ResetCachesStrategy resetCachesStrategy;

/**
* Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default
* {@link RedisCacheConfiguration}.
Expand Down Expand Up @@ -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<String, RedisCacheConfiguration> 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;
}

/**
Expand Down Expand Up @@ -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}.
*
Expand Down Expand Up @@ -312,6 +331,16 @@ public boolean isAllowRuntimeCacheCreation() {
return this.allowRuntimeCacheCreation;
}

@Override
public void resetCaches() {

if(resetCachesStrategy instanceof CacheWriterOperation<?> operation) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first thought was to abstract the impl down into the strategy via a resetCaches method on the strategy where the strategy could take in a cache writer and a cache manager. The flushing variant could use the cache writer and the default variant could use the CM resetCaches impl. However, the latter would call back into CacheManager.resetCaches and that would infinite recurse (i.e. not get to super.resetCaches).

Just putting my thoughts - I am not sure which direction to go but I do like the thought of the strategy handling the work.

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}.
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -342,6 +345,10 @@ default boolean invalidate(String name, byte[] pattern) {
*/
void clearStatistics(String name);

default <T> T execute(Function<RedisConnection, T> callback) {
throw new UnsupportedOperationException("execute(...) is not supported by this RedisCacheWriter");
}

/**
* Obtain a {@link RedisCacheWriter} using the given {@link CacheStatisticsCollector} to collect metrics.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may read better as CacheResetStrategy/ies - wdyt?

Uggh, naming... always hard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just ResetStrategy? Cache defines clear and evict methods and reset is distinct from these (thus it ranges semantically in a similar space).

Design-wise, it could be an interface with two static methods, sequential() (the default) and flushDb(). I like the CacheWriterOperation approach as it repeats how we approach functional composition in other parts of the framework.


public interface ResetCachesStrategy {}

public static ResetCachesStrategy oneByOne() {
return DefaultResetStrategy.INSTANCE;
}

public static ResetCachesStrategy flushing() {
return FlushingResetStrategy.INSTANCE;
}

enum FlushingResetStrategy implements ResetCachesStrategy, CacheWriterOperation<String> {

INSTANCE;

@Override
public String doWithCacheWriter(RedisCacheWriter cacheWriter) {
return cacheWriter.execute(connection -> {
connection.serverCommands().flushDb(FlushOption.ASYNC);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any other cleanup / stats we are missing that the super.resetCaches may be doing that we should do here as well?

return "ok";
});
}
}

enum DefaultResetStrategy implements ResetCachesStrategy {
INSTANCE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<Function<RedisConnection, Object>> 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());
}
}
Loading