I am setting up a Spring Boot 3 application (using Gradle and Java 17) with Redis Sentinel and the Lettuce client (non-reactive, with RedisTemplate).
Our infrastructure team has provided a Redis Sentinel setup (1 node) with a master and a replica, along with a dedicated user and password(+ACL). However, on application startup, we encounter a health check failure.
PoolException: Could not get a resource from the pool
RedisConnectionException: Unable to connect to redis-sentinel://test:****@<ADDRESS>?sentinelMasterId=<MasterName>&timeout=3s
RedisCommandExecutionException: NOPERM User test has no permissions to run the 'sentinel|replicas' command.
The infrastructure team confirms they have intentionally restricted this command, because they believe it serves a reactive purpose, where they found a bug that wasn't fixed in recent versions...
They also suggested that Lettuce can be configured not to use this command. Is this true?
How can I configure the Lettuce client in a non-reactive Spring Boot application to work without requiring the SENTINEL REPLICAS permission? Are there specific settings, like the ReadFrom strategy or an alternative configuration method, that would avoid this command?
I'm sure this is standard behavior in both reactive and non-reactive Lettuce modes, and you can't do without this command.
My configuration: I am using a standard RedisSentinelConfiguration to set up the LettuceConnectionFactory and ReadFrom.Master.
@Configuration
@ConditionalOnProperty(prefix = "app.cache.redis", name = "enabled", havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class RedisConfig {
private final CustomRedisProperties customRedisProperties;
@Bean(destroyMethod = "shutdown")
public ClientResources clientResources() {
return DefaultClientResources.create();
}
@Bean
public ClientOptions clientOptions() {
return ClientOptions.builder()
.timeoutOptions(TimeoutOptions.enabled(Duration.ofMillis(customRedisProperties.getConnectTimeout())))
.disconnectedBehavior(DisconnectedBehavior.REJECT_COMMANDS)
.pingBeforeActivateConnection(true)
.autoReconnect(true)
.build();
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.build();
}
@Bean
public GenericObjectPoolConfig<?> redisPoolConfig() {
GenericObjectPoolConfig<?> genericObjectPoolConfig = new GenericObjectPoolConfig<>();
genericObjectPoolConfig.setMaxTotal(customRedisProperties.getMaxActive());
genericObjectPoolConfig.setMaxIdle(customRedisProperties.getMaxIdle());
genericObjectPoolConfig.setMinIdle(customRedisProperties.getMinIdle());
genericObjectPoolConfig.setMaxWait(Duration.ofMillis(customRedisProperties.getMaxWait()));
genericObjectPoolConfig.setTestOnBorrow(true);
genericObjectPoolConfig.setTestWhileIdle(true);
genericObjectPoolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30));
genericObjectPoolConfig.setMinEvictableIdleDuration(Duration.ofSeconds(60));
genericObjectPoolConfig.setNumTestsPerEvictionRun(3);
return genericObjectPoolConfig;
}
@Bean
public LettuceClientConfiguration lettuceClientConfiguration(
GenericObjectPoolConfig<?> redisPoolConfig,
ClientOptions clientOptions,
ClientResources clientResources
) {
return LettucePoolingClientConfiguration.builder()
.poolConfig(redisPoolConfig)
.clientOptions(clientOptions)
.clientResources(clientResources)
.commandTimeout(Duration.ofMillis(customRedisProperties.getCommandTimeout()))
.readFrom(ReadFrom.UPSTREAM) //ReadFrom.MASTER
.build();
}
@Bean
public RedisConnectionFactory redisConnectionFactory(
LettuceClientConfiguration lettuceClientConfiguration
) {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
sentinelConfig.master(customRedisProperties.getSentinel().getMaster());
if (Objects.nonNull(customRedisProperties.getSentinel().getPassword())) {
sentinelConfig.setSentinelPassword(RedisPassword.of(customRedisProperties.getSentinel().getPassword()));
}
if (Objects.nonNull(customRedisProperties.getSentinel().getUsername())) {
sentinelConfig.setSentinelUsername(customRedisProperties.getSentinel().getUsername());
}
if (customRedisProperties.getSupportUsername()
&& Objects.nonNull(customRedisProperties.getUsername())) {
sentinelConfig.setUsername(customRedisProperties.getUsername());
}
if (Objects.nonNull(customRedisProperties.getPassword())) {
sentinelConfig.setPassword(RedisPassword.of(customRedisProperties.getPassword()));
}
customRedisProperties.getSentinel().getNodes().forEach(node -> {
final var parts = node.split(":");
sentinelConfig.sentinel(parts[0], Integer.parseInt(parts[1]));
});
final var factory = new LettuceConnectionFactory(sentinelConfig, lettuceClientConfiguration);
factory.setShareNativeConnection(false);
factory.setValidateConnection(false);
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory,
ObjectMapper objectMapper) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
final var serializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
I solved this problem:
When you explicitly configure ReadFrom.UPSTREAM (or any ReadFrom strategy):
When you remove the ReadFrom configuration: