Skip to content

Commit fd11598

Browse files
nosansnicoll
authored andcommitted
Add spring.data.redis.lettuce.read-from property
See gh-42588
1 parent 9430a47 commit fd11598

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

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();

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
}

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
@@ -2924,6 +2924,52 @@
29242924
}
29252925
]
29262926
},
2927+
{
2928+
"name": "spring.data.redis.lettuce.read-from",
2929+
"values": [
2930+
{
2931+
"value": "any",
2932+
"description": "Read from any node."
2933+
},
2934+
{
2935+
"value": "any-replica",
2936+
"description": "Read from any replica node."
2937+
},
2938+
{
2939+
"value": "lowest-latency",
2940+
"description": "Read from the node with the lowest latency during topology discovery."
2941+
},
2942+
{
2943+
"value": "regex:",
2944+
"description": "Read from any node that has RedisURI matching with the given pattern."
2945+
},
2946+
{
2947+
"value": "replica",
2948+
"description": "Read from the replica only."
2949+
},
2950+
{
2951+
"value": "replica-preferred",
2952+
"description": "Read preferred from replica and fall back to upstream if no replica is available."
2953+
},
2954+
{
2955+
"value": "subnet:",
2956+
"description": "Read from any node in the subnets."
2957+
},
2958+
{
2959+
"value": "upstream",
2960+
"description": "Read from the upstream only."
2961+
},
2962+
{
2963+
"value": "upstream-preferred",
2964+
"description": "Read preferred from the upstream and fall back to a replica if the upstream is not available."
2965+
}
2966+
],
2967+
"providers": [
2968+
{
2969+
"name": "any"
2970+
}
2971+
]
2972+
},
29272973
{
29282974
"name": "spring.datasource.data",
29292975
"providers": [

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

+90
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,30 @@
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;
3340
import org.junit.jupiter.api.Test;
3441
import org.junit.jupiter.api.condition.EnabledForJreRange;
3542
import org.junit.jupiter.api.condition.JRE;
43+
import org.junit.jupiter.params.ParameterizedTest;
44+
import org.junit.jupiter.params.provider.Arguments;
45+
import org.junit.jupiter.params.provider.MethodSource;
3646

3747
import org.springframework.boot.autoconfigure.AutoConfigurations;
3848
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool;
@@ -112,6 +122,52 @@ void testOverrideRedisConfiguration() {
112122
});
113123
}
114124

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

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

0 commit comments

Comments
 (0)