Skip to content

Commit be2956a

Browse files
committed
Add spring.redis.lettuce.read-from property
1 parent d815cad commit be2956a

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.boot.autoconfigure.data.redis;
1818

1919
import java.time.Duration;
20+
import java.util.regex.Pattern;
2021

2122
import io.lettuce.core.ClientOptions;
23+
import io.lettuce.core.ReadFrom;
2224
import io.lettuce.core.RedisClient;
2325
import io.lettuce.core.SocketOptions;
2426
import io.lettuce.core.TimeoutOptions;
@@ -51,6 +53,7 @@
5153
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
5254
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
5355
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
56+
import org.springframework.util.Assert;
5457
import org.springframework.util.StringUtils;
5558

5659
/**
@@ -163,6 +166,10 @@ private void applyProperties(LettuceClientConfiguration.LettuceClientConfigurati
163166
if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
164167
builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout());
165168
}
169+
RedisProperties.Lettuce.ReadFrom readFrom = lettuce.getReadFrom();
170+
if (readFrom.getType() != null) {
171+
builder.readFrom(getReadFrom(readFrom));
172+
}
166173
}
167174
if (StringUtils.hasText(getProperties().getClientName())) {
168175
builder.clientName(getProperties().getClientName());
@@ -218,6 +225,24 @@ private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceCli
218225
}
219226
}
220227

228+
private static ReadFrom getReadFrom(RedisProperties.Lettuce.ReadFrom readFrom) {
229+
return switch (readFrom.getType()) {
230+
case ANY -> ReadFrom.ANY;
231+
case ANY_REPLICA -> ReadFrom.ANY_REPLICA;
232+
case LOWEST_LATENCY -> ReadFrom.LOWEST_LATENCY;
233+
case REGEX -> {
234+
Assert.notNull(readFrom.getPattern(),
235+
"Regex pattern must be present when type is '" + readFrom.getType() + "'");
236+
yield ReadFrom.regex(Pattern.compile(readFrom.getPattern()));
237+
}
238+
case REPLICA -> ReadFrom.REPLICA;
239+
case REPLICA_PREFERRED -> ReadFrom.REPLICA_PREFERRED;
240+
case SUBNET -> ReadFrom.subnet(readFrom.getCidrNotations().toArray(new String[0]));
241+
case UPSTREAM -> ReadFrom.UPSTREAM;
242+
case UPSTREAM_PREFERRED -> ReadFrom.UPSTREAM_PREFERRED;
243+
};
244+
}
245+
221246
/**
222247
* Inner class to allow optional commons-pool2 dependency.
223248
*/

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

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.autoconfigure.data.redis;
1818

1919
import java.time.Duration;
20+
import java.util.ArrayList;
2021
import java.util.List;
2122

2223
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -467,6 +468,11 @@ public static class Lettuce {
467468
*/
468469
private Duration shutdownTimeout = Duration.ofMillis(100);
469470

471+
/**
472+
* Defines from which Redis nodes data is read.
473+
*/
474+
private final ReadFrom readFrom = new ReadFrom();
475+
470476
/**
471477
* Lettuce pool configuration.
472478
*/
@@ -482,6 +488,10 @@ public void setShutdownTimeout(Duration shutdownTimeout) {
482488
this.shutdownTimeout = shutdownTimeout;
483489
}
484490

491+
public ReadFrom getReadFrom() {
492+
return this.readFrom;
493+
}
494+
485495
public Pool getPool() {
486496
return this.pool;
487497
}
@@ -490,6 +500,99 @@ public Cluster getCluster() {
490500
return this.cluster;
491501
}
492502

503+
public static class ReadFrom {
504+
505+
/**
506+
* Type from which Redis nodes data is read.
507+
*/
508+
private Type type;
509+
510+
/**
511+
* CIDR-block notations to use in conjunction with SUBNET type.
512+
*/
513+
private final List<String> cidrNotations = new ArrayList<>();
514+
515+
/**
516+
* The Regex pattern to use in conjunction with REGEX type.
517+
*/
518+
private String pattern;
519+
520+
public Type getType() {
521+
return this.type;
522+
}
523+
524+
public void setType(Type type) {
525+
this.type = type;
526+
}
527+
528+
public List<String> getCidrNotations() {
529+
return this.cidrNotations;
530+
}
531+
532+
public String getPattern() {
533+
return this.pattern;
534+
}
535+
536+
public void setPattern(String pattern) {
537+
this.pattern = pattern;
538+
}
539+
540+
public enum Type {
541+
542+
/**
543+
* Read from any node.
544+
*
545+
*/
546+
ANY,
547+
/**
548+
* Read from any replica node.
549+
*/
550+
ANY_REPLICA,
551+
/**
552+
* Read from the node with the lowest latency during topology discovery.
553+
* Note that latency measurements are momentary snapshots that can change
554+
* in rapid succession. Requires dynamic refresh sources to obtain
555+
* topologies and latencies from all nodes in the cluster.
556+
*
557+
*/
558+
LOWEST_LATENCY,
559+
560+
/**
561+
* Read from any node that has RedisURI matching with the given pattern.
562+
*/
563+
REGEX,
564+
/**
565+
* Read from the replica only.
566+
*
567+
*/
568+
REPLICA,
569+
/**
570+
* Read preferred from replica and fall back to upstream if no replica is
571+
* available.
572+
*
573+
*/
574+
REPLICA_PREFERRED,
575+
/**
576+
* Read from any node in the subnets.
577+
*/
578+
SUBNET,
579+
580+
/**
581+
* Read from the upstream only.
582+
*
583+
*/
584+
UPSTREAM,
585+
/**
586+
* Read preferred from the upstream and fall back to a replica if the
587+
* upstream is not available.
588+
*
589+
*/
590+
UPSTREAM_PREFERRED
591+
592+
}
593+
594+
}
595+
493596
public static class Cluster {
494597

495598
private final Refresh refresh = new Refresh();

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,32 @@
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.models.role.RedisNodeDescription;
3036
import io.lettuce.core.resource.DefaultClientResources;
3137
import io.lettuce.core.tracing.Tracing;
3238
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
3339
import org.junit.jupiter.api.Test;
3440
import org.junit.jupiter.api.condition.EnabledForJreRange;
3541
import org.junit.jupiter.api.condition.JRE;
42+
import org.junit.jupiter.params.ParameterizedTest;
43+
import org.junit.jupiter.params.provider.Arguments;
44+
import org.junit.jupiter.params.provider.MethodSource;
3645

3746
import org.springframework.boot.autoconfigure.AutoConfigurations;
47+
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce;
3848
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool;
3949
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
4050
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
@@ -62,6 +72,7 @@
6272

6373
import static org.assertj.core.api.Assertions.assertThat;
6474
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
75+
import static org.mockito.BDDMockito.given;
6576
import static org.mockito.Mockito.mock;
6677

6778
/**
@@ -112,6 +123,57 @@ void testOverrideRedisConfiguration() {
112123
});
113124
}
114125

126+
@ParameterizedTest
127+
@MethodSource
128+
void shouldConfigureLettuceReadFromProperty(Lettuce.ReadFrom.Type when, io.lettuce.core.ReadFrom expected) {
129+
this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from.type:" + when).run((context) -> {
130+
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
131+
assertThat(cf.getClientConfiguration().getReadFrom()).hasValue(expected);
132+
});
133+
}
134+
135+
@Test
136+
void shouldConfigureLettuceReadFromPropertyRegexType() {
137+
RedisNodeDescription node1 = mock(RedisNodeDescription.class);
138+
given(node1.getUri()).willReturn(new RedisURI("127.0.0.1", 6379, Duration.ZERO));
139+
RedisNodeDescription node2 = mock(RedisNodeDescription.class);
140+
given(node2.getUri()).willReturn(new RedisURI("192.12.128.0", 6379, Duration.ZERO));
141+
RedisNodeDescription node3 = mock(RedisNodeDescription.class);
142+
given(node3.getUri()).willReturn(new RedisURI("192.168.255.255", 6379, Duration.ZERO));
143+
this.contextRunner
144+
.withPropertyValues("spring.data.redis.lettuce.read-from.type:" + Lettuce.ReadFrom.Type.REGEX,
145+
"spring.data.redis.lettuce.read-from.pattern:192.*")
146+
.run((context) -> {
147+
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
148+
assertThat(cf.getClientConfiguration().getReadFrom()).isNotEmpty();
149+
ReadFrom readFrom = cf.getClientConfiguration().getReadFrom().get();
150+
List<RedisNodeDescription> selected = readFrom.select(new RedisNodes(node1, node2, node3));
151+
assertThat(selected).hasSize(2);
152+
assertThat(selected).contains(node2, node3);
153+
});
154+
}
155+
156+
@Test
157+
void shouldConfigureLettuceReadFromPropertySubnetType() {
158+
RedisNodeDescription node1 = mock(RedisNodeDescription.class);
159+
given(node1.getUri()).willReturn(new RedisURI("127.0.0.1", 6379, Duration.ZERO));
160+
RedisNodeDescription node2 = mock(RedisNodeDescription.class);
161+
given(node2.getUri()).willReturn(new RedisURI("192.12.128.0", 6379, Duration.ZERO));
162+
RedisNodeDescription node3 = mock(RedisNodeDescription.class);
163+
given(node3.getUri()).willReturn(new RedisURI("192.168.255.255", 6379, Duration.ZERO));
164+
this.contextRunner
165+
.withPropertyValues("spring.data.redis.lettuce.read-from.type:" + Lettuce.ReadFrom.Type.SUBNET,
166+
"spring.data.redis.lettuce.read-from.cidr-notations:192.12.128.0/32,192.168.255.255/32")
167+
.run((context) -> {
168+
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
169+
assertThat(cf.getClientConfiguration().getReadFrom()).isNotEmpty();
170+
ReadFrom readFrom = cf.getClientConfiguration().getReadFrom().get();
171+
List<RedisNodeDescription> selected = readFrom.select(new RedisNodes(node1, node2, node3));
172+
assertThat(selected).hasSize(2);
173+
assertThat(selected).contains(node2, node3);
174+
});
175+
}
176+
115177
@Test
116178
void testCustomizeClientResources() {
117179
Tracing tracing = mock(Tracing.class);
@@ -632,6 +694,36 @@ private String getUserName(LettuceConnectionFactory factory) {
632694
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
633695
}
634696

697+
static Stream<Arguments> shouldConfigureLettuceReadFromProperty() {
698+
return Stream.of(Arguments.of(Lettuce.ReadFrom.Type.ANY, ReadFrom.ANY),
699+
Arguments.of(Lettuce.ReadFrom.Type.ANY_REPLICA, ReadFrom.ANY_REPLICA),
700+
Arguments.of(Lettuce.ReadFrom.Type.LOWEST_LATENCY, ReadFrom.LOWEST_LATENCY),
701+
Arguments.of(Lettuce.ReadFrom.Type.REPLICA, ReadFrom.REPLICA),
702+
Arguments.of(Lettuce.ReadFrom.Type.REPLICA_PREFERRED, ReadFrom.REPLICA_PREFERRED),
703+
Arguments.of(Lettuce.ReadFrom.Type.UPSTREAM, ReadFrom.UPSTREAM),
704+
Arguments.of(Lettuce.ReadFrom.Type.UPSTREAM_PREFERRED, ReadFrom.UPSTREAM_PREFERRED));
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)