Skip to content

Commit 2ae3070

Browse files
committed
Add spring.redis.lettuce.read-from property
1 parent 5321d46 commit 2ae3070

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-0
lines changed

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java

+16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.time.Duration;
2020

2121
import io.lettuce.core.ClientOptions;
22+
import io.lettuce.core.ReadFrom;
2223
import io.lettuce.core.RedisClient;
2324
import io.lettuce.core.SocketOptions;
2425
import io.lettuce.core.TimeoutOptions;
@@ -163,12 +164,27 @@ private void applyProperties(LettuceClientConfiguration.LettuceClientConfigurati
163164
if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
164165
builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout());
165166
}
167+
String readFrom = lettuce.getReadFrom();
168+
if (readFrom != null) {
169+
builder.readFrom(getReadFrom(readFrom));
170+
}
166171
}
167172
if (StringUtils.hasText(getProperties().getClientName())) {
168173
builder.clientName(getProperties().getClientName());
169174
}
170175
}
171176

177+
private static ReadFrom getReadFrom(String readFrom) {
178+
int index = readFrom.indexOf(':');
179+
if (index == -1) {
180+
String name = readFrom.replaceAll("-", "");
181+
return ReadFrom.valueOf(name);
182+
}
183+
String name = readFrom.substring(0, index).replaceAll("-", "");
184+
String value = readFrom.substring(index + 1);
185+
return ReadFrom.valueOf(name + ":" + value);
186+
}
187+
172188
private ClientOptions createClientOptions(
173189
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientConfigurationBuilderCustomizers) {
174190
ClientOptions.Builder builder = initializeClientOptionsBuilder();

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java

+13
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,11 @@ public static class Lettuce {
467467
*/
468468
private Duration shutdownTimeout = Duration.ofMillis(100);
469469

470+
/**
471+
* Defines from which Redis nodes data is read.
472+
*/
473+
private String readFrom;
474+
470475
/**
471476
* Lettuce pool configuration.
472477
*/
@@ -482,6 +487,14 @@ public void setShutdownTimeout(Duration shutdownTimeout) {
482487
this.shutdownTimeout = shutdownTimeout;
483488
}
484489

490+
public void setReadFrom(String readFrom) {
491+
this.readFrom = readFrom;
492+
}
493+
494+
public String getReadFrom() {
495+
return this.readFrom;
496+
}
497+
485498
public Pool getPool() {
486499
return this.pool;
487500
}

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

+46
Original file line numberDiff line numberDiff line change
@@ -2913,6 +2913,52 @@
29132913
}
29142914
]
29152915
},
2916+
{
2917+
"name": "spring.redis.lettuce.read-from",
2918+
"values": [
2919+
{
2920+
"value": "any",
2921+
"description": "Read from any node."
2922+
},
2923+
{
2924+
"value": "any-replica",
2925+
"description": "Read from any replica node."
2926+
},
2927+
{
2928+
"value": "lowest-latency",
2929+
"description": "Read from the node with the lowest latency during topology discovery."
2930+
},
2931+
{
2932+
"value": "regex:",
2933+
"description": "Read from any node that has RedisURI matching with the given pattern"
2934+
},
2935+
{
2936+
"value": "replica",
2937+
"description": "Read from the replica only."
2938+
},
2939+
{
2940+
"value": "replica-preferred",
2941+
"description": "Read preferred from replica and fall back to upstream if no replica is available"
2942+
},
2943+
{
2944+
"value": "subnet:",
2945+
"description": "Read from any node in the subnets."
2946+
},
2947+
{
2948+
"value": "upstream",
2949+
"description": "Read from the upstream only."
2950+
},
2951+
{
2952+
"value": "upstream-preferred",
2953+
"description": "Read preferred from the upstream and fall back to a replica if the upstream is not available."
2954+
}
2955+
],
2956+
"providers": [
2957+
{
2958+
"name": "any"
2959+
}
2960+
]
2961+
},
29162962
{
29172963
"name": "spring.data.mongodb.field-naming-strategy",
29182964
"providers": [

Diff for: spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java

+92
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,31 @@
1919
import java.time.Duration;
2020
import java.util.Arrays;
2121
import java.util.EnumSet;
22+
import java.util.Iterator;
2223
import java.util.List;
2324
import java.util.Set;
2425
import java.util.function.Consumer;
2526
import java.util.stream.Collectors;
27+
import java.util.stream.Stream;
2628

2729
import io.lettuce.core.ClientOptions;
30+
import io.lettuce.core.ReadFrom;
31+
import io.lettuce.core.ReadFrom.Nodes;
32+
import io.lettuce.core.RedisURI;
2833
import io.lettuce.core.cluster.ClusterClientOptions;
2934
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger;
35+
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
36+
import io.lettuce.core.models.role.RedisNodeDescription;
3037
import io.lettuce.core.resource.DefaultClientResources;
3138
import io.lettuce.core.tracing.Tracing;
3239
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
40+
import org.junit.jupiter.api.Disabled;
3341
import org.junit.jupiter.api.Test;
3442
import org.junit.jupiter.api.condition.EnabledForJreRange;
3543
import org.junit.jupiter.api.condition.JRE;
44+
import org.junit.jupiter.params.ParameterizedTest;
45+
import org.junit.jupiter.params.provider.Arguments;
46+
import org.junit.jupiter.params.provider.MethodSource;
3647

3748
import org.springframework.boot.autoconfigure.AutoConfigurations;
3849
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool;
@@ -112,6 +123,53 @@ void testOverrideRedisConfiguration() {
112123
});
113124
}
114125

126+
@ParameterizedTest
127+
@MethodSource
128+
void shouldConfigureLettuceReadFromProperty(String type, ReadFrom readFrom) {
129+
this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:" + type).run((context) -> {
130+
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
131+
assertThat(cf.getClientConfiguration().getReadFrom()).hasValue(readFrom);
132+
});
133+
}
134+
135+
@Test
136+
@Disabled("Lettuce 6.5.0.RELEASE")
137+
void shouldConfigureLettuceReadFromPropertyRegexType() {
138+
RedisClusterNode node1 = createRedisNode("redis-node-1.region-1.example.com");
139+
RedisClusterNode node2 = createRedisNode("redis-node-2.region-1.example.com");
140+
RedisClusterNode node3 = createRedisNode("redis-node-1.region-2.example.com");
141+
RedisClusterNode node4 = createRedisNode("redis-node-2.region-2.example.com");
142+
this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:regex:.*region-1.*")
143+
.run((context) -> {
144+
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
145+
LettuceClientConfiguration configuration = factory.getClientConfiguration();
146+
assertThat(configuration.getReadFrom()).hasValueSatisfying((readFrom) -> {
147+
List<RedisNodeDescription> result = readFrom.select(new RedisNodes(node1, node2, node3, node4));
148+
assertThat(result).hasSize(2).containsExactly(node1, node2);
149+
});
150+
});
151+
}
152+
153+
@Test
154+
@Disabled("Lettuce 6.5.0.RELEASE")
155+
void shouldConfigureLettuceReadFromPropertySubnetType() {
156+
RedisClusterNode nodeInSubnetIpv4 = createRedisNode("192.0.2.1");
157+
RedisClusterNode nodeNotInSubnetIpv4 = createRedisNode("198.51.100.1");
158+
RedisClusterNode nodeInSubnetIpv6 = createRedisNode("2001:db8:abcd:0000::1");
159+
RedisClusterNode nodeNotInSubnetIpv6 = createRedisNode("2001:db8:abcd:1000::");
160+
this.contextRunner
161+
.withPropertyValues("spring.data.redis.lettuce.read-from:subnet:192.0.2.0/24,2001:db8:abcd:0000::/52")
162+
.run((context) -> {
163+
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
164+
LettuceClientConfiguration configuration = factory.getClientConfiguration();
165+
assertThat(configuration.getReadFrom()).hasValueSatisfying((readFrom) -> {
166+
List<RedisNodeDescription> result = readFrom.select(new RedisNodes(nodeInSubnetIpv4,
167+
nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
168+
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
169+
});
170+
});
171+
}
172+
115173
@Test
116174
void testCustomizeClientResources() {
117175
Tracing tracing = mock(Tracing.class);
@@ -632,6 +690,40 @@ private String getUserName(LettuceConnectionFactory factory) {
632690
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
633691
}
634692

693+
static Stream<Arguments> shouldConfigureLettuceReadFromProperty() {
694+
return Stream.of(Arguments.of("any", ReadFrom.ANY), Arguments.of("any-replica", ReadFrom.ANY_REPLICA),
695+
Arguments.of("lowest-latency", ReadFrom.LOWEST_LATENCY), Arguments.of("replica", ReadFrom.REPLICA),
696+
Arguments.of("replica-preferred", ReadFrom.REPLICA_PREFERRED),
697+
Arguments.of("upstream", ReadFrom.UPSTREAM),
698+
Arguments.of("upstream-preferred", ReadFrom.UPSTREAM_PREFERRED));
699+
}
700+
701+
private RedisClusterNode createRedisNode(String host) {
702+
RedisClusterNode node = new RedisClusterNode();
703+
node.setUri(RedisURI.Builder.redis(host).build());
704+
return node;
705+
}
706+
707+
private static final class RedisNodes implements Nodes {
708+
709+
private final List<RedisNodeDescription> descriptions;
710+
711+
RedisNodes(RedisNodeDescription... descriptions) {
712+
this.descriptions = List.of(descriptions);
713+
}
714+
715+
@Override
716+
public List<RedisNodeDescription> getNodes() {
717+
return this.descriptions;
718+
}
719+
720+
@Override
721+
public Iterator<RedisNodeDescription> iterator() {
722+
return this.descriptions.iterator();
723+
}
724+
725+
}
726+
635727
@Configuration(proxyBeanMethods = false)
636728
static class CustomConfiguration {
637729

0 commit comments

Comments
 (0)