Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ private JedisConnectionFactory createJedisConnectionFactory(
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
yield new JedisConnectionFactory(sentinelConfig, clientConfiguration);
}
case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master replica is not supported for Jedis");
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package org.springframework.boot.data.redis.autoconfigure;

import java.time.Duration;
import java.util.Collections;
import java.util.List;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom;
Expand All @@ -37,6 +39,8 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Node;
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce;
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce.Cluster.Refresh;
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Pool;
import org.springframework.boot.ssl.SslBundle;
Expand All @@ -49,11 +53,13 @@
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
Expand All @@ -64,6 +70,7 @@
* @author Moritz Halbritter
* @author Phillip Webb
* @author Scott Frederick
* @author Yong-Hyun Kim
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
Expand Down Expand Up @@ -120,6 +127,12 @@ private LettuceConnectionFactory createConnectionFactory(
LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration(
clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources,
getProperties().getLettuce().getPool());

RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration();
if (staticMasterReplicaConfiguration != null) {
return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration);
}

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the code move to a new Mode value. It feels odd that there is a if check right before we switch over all the supported modes.

return switch (this.mode) {
case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
case CLUSTER -> {
Expand All @@ -132,9 +145,34 @@ private LettuceConnectionFactory createConnectionFactory(
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration);
}
case STATIC_MASTER_REPLICA -> {
RedisStaticMasterReplicaConfiguration configuration = getStaticMasterReplicaConfiguration();
Assert.state(configuration != null, "'staticMasterReplicaConfiguration' must not be null");
yield new LettuceConnectionFactory(configuration, clientConfiguration);
}
};
}

private @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() {
RedisProperties.Lettuce lettuce = getProperties().getLettuce();

if (!CollectionUtils.isEmpty(lettuce.getNodes())) {
List<Node> nodes = asNodes(lettuce.getNodes());
RedisStaticMasterReplicaConfiguration configuration = new RedisStaticMasterReplicaConfiguration(
nodes.get(0).host(), nodes.get(0).port());
configuration.setUsername(getProperties().getUsername());
if (StringUtils.hasText(getProperties().getPassword())) {
configuration.setPassword(getProperties().getPassword());
}
configuration.setDatabase(getProperties().getDatabase());
nodes.stream().skip(1).forEach((node) -> configuration.addNode(node.host(), node.port()));

return configuration;
}

return null;
}

private LettuceClientConfiguration getLettuceClientConfiguration(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
Expand Down Expand Up @@ -250,6 +288,20 @@ private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceCli
}
}

private List<Node> asNodes(@Nullable List<String> nodes) {
if (nodes == null) {
return Collections.emptyList();
}
return nodes.stream().map(this::asNode).toList();
}

private Node asNode(String node) {
int portSeparatorIndex = node.lastIndexOf(':');
String host = node.substring(0, portSeparatorIndex);
int port = Integer.parseInt(node.substring(portSeparatorIndex + 1));
return new Node(host, port);
}

/**
* Inner class to allow optional commons-pool2 dependency.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

/**
* Base Redis connection configuration.
Expand All @@ -48,6 +49,7 @@
* @author Andy Wilkinson
* @author Phillip Webb
* @author Yanming Zhou
* @author Yong-Hyun Kim
*/
abstract class RedisConnectionConfiguration {

Expand Down Expand Up @@ -156,7 +158,7 @@ protected final RedisProperties getProperties() {

protected @Nullable SslBundle getSslBundle() {
return switch (this.mode) {
case STANDALONE -> (this.connectionDetails.getStandalone() != null)
case STANDALONE, STATIC_MASTER_REPLICA -> (this.connectionDetails.getStandalone() != null)
? this.connectionDetails.getStandalone().getSslBundle() : null;
case CLUSTER -> (this.connectionDetails.getCluster() != null)
? this.connectionDetails.getCluster().getSslBundle() : null;
Expand Down Expand Up @@ -197,12 +199,15 @@ private Mode determineMode() {
if (getClusterConfiguration() != null) {
return Mode.CLUSTER;
}
if (!CollectionUtils.isEmpty(this.properties.getLettuce().getNodes())) {
return Mode.STATIC_MASTER_REPLICA;
}
return Mode.STANDALONE;
}

enum Mode {

STANDALONE, CLUSTER, SENTINEL
STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* @author Stephane Nicoll
* @author Scott Frederick
* @author Yanming Zhou
* @author Yong-Hyun Kim
* @since 4.0.0
*/
@ConfigurationProperties("spring.data.redis")
Expand Down Expand Up @@ -482,6 +483,20 @@ public static class Lettuce {

private final Cluster cluster = new Cluster();

/**
* List of static master-replica "host:port" pairs regardless of role
* as the actual roles are determined by querying each node's ROLE command.
*/
private @Nullable List<String> nodes;

public @Nullable List<String> getNodes() {
return this.nodes;
}

public void setNodes(@Nullable List<String> nodes) {
this.nodes = nodes;
}

public Duration getShutdownTimeout() {
return this.shutdownTimeout;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Yong-Hyun Kim
*/
class RedisAutoConfigurationTests {

Expand Down Expand Up @@ -501,6 +502,38 @@ void testRedisConfigurationWithClusterAndAuthentication() {
);
}

@Test
void testRedisConfigurationWithStaticMasterReplica() {
List<String> staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320", "[::1]:28321");
this.contextRunner
.withPropertyValues(
"spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0),
"spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1),
"spring.data.redis.lettuce.static-master-replica.nodes[2]:" + staticMasterReplicaNodes.get(2))
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(connectionFactory.getSentinelConfiguration()).isNull();
assertThat(connectionFactory.getClusterConfiguration()).isNull();
assertThat(isStaticMasterReplicaAware(connectionFactory)).isTrue();
});
}

@Test
void testRedisConfigurationWithStaticMasterReplicaAndAuthenticationAndDatabase() {
List<String> staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320");
this.contextRunner
.withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password",
"spring.data.redis.database=1",
"spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0),
"spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1))
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(getUserName(connectionFactory)).isEqualTo("user");
assertThat(connectionFactory.getPassword()).isEqualTo("password");
assertThat(connectionFactory.getDatabase()).isOne();
});
}

@Test
void testRedisConfigurationCreateClientOptionsByDefault() {
this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> {
Expand Down Expand Up @@ -704,6 +737,10 @@ private RedisClusterNode createRedisNode(String host) {
return node;
}

private boolean isStaticMasterReplicaAware(LettuceConnectionFactory factory) {
return ReflectionTestUtils.invokeMethod(factory, "isStaticMasterReplicaAware");
}

private static final class RedisNodes implements Nodes {

private final List<RedisNodeDescription> descriptions;
Expand Down