From 7b7de11124780e1789541de178c7092ad7e5a7ad Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Sat, 16 Nov 2024 14:33:52 +0200 Subject: [PATCH 1/3] Upgrade to Lettuce 6.5.0.RELEASE --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3167c8073418..66a86f03c593 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1169,7 +1169,7 @@ bom { releaseNotes("https://github.com/Kotlin/kotlinx.serialization/releases/tag/v{version}") } } - library("Lettuce", "6.4.1.RELEASE") { + library("Lettuce", "6.5.0.RELEASE") { group("io.lettuce") { modules = [ "lettuce-core" From 57f0e187457afd1280b77dbc49af6727565c43c9 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 10 Oct 2024 16:14:18 +0300 Subject: [PATCH 2/3] Add spring.data.redis.lettuce.read-from property --- .../redis/LettuceConnectionConfiguration.java | 16 ++++ .../data/redis/RedisProperties.java | 13 +++ ...itional-spring-configuration-metadata.json | 46 ++++++++++ .../redis/RedisAutoConfigurationTests.java | 90 +++++++++++++++++++ 4 files changed, 165 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index 040eebd919cd..c47bf7cf72f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -19,6 +19,7 @@ import java.time.Duration; import io.lettuce.core.ClientOptions; +import io.lettuce.core.ReadFrom; import io.lettuce.core.RedisClient; import io.lettuce.core.SocketOptions; import io.lettuce.core.TimeoutOptions; @@ -163,12 +164,27 @@ private void applyProperties(LettuceClientConfiguration.LettuceClientConfigurati if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) { builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout()); } + String readFrom = lettuce.getReadFrom(); + if (readFrom != null) { + builder.readFrom(getReadFrom(readFrom)); + } } if (StringUtils.hasText(getProperties().getClientName())) { builder.clientName(getProperties().getClientName()); } } + private static ReadFrom getReadFrom(String readFrom) { + int index = readFrom.indexOf(':'); + if (index == -1) { + String name = readFrom.replaceAll("-", ""); + return ReadFrom.valueOf(name); + } + String name = readFrom.substring(0, index).replaceAll("-", ""); + String value = readFrom.substring(index + 1); + return ReadFrom.valueOf(name + ":" + value); + } + private ClientOptions createClientOptions( ObjectProvider clientConfigurationBuilderCustomizers) { ClientOptions.Builder builder = initializeClientOptionsBuilder(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java index 8e5f07eb5bf3..aefe9e5a7ed4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java @@ -467,6 +467,11 @@ public static class Lettuce { */ private Duration shutdownTimeout = Duration.ofMillis(100); + /** + * Defines from which Redis nodes data is read. + */ + private String readFrom; + /** * Lettuce pool configuration. */ @@ -482,6 +487,14 @@ public void setShutdownTimeout(Duration shutdownTimeout) { this.shutdownTimeout = shutdownTimeout; } + public void setReadFrom(String readFrom) { + this.readFrom = readFrom; + } + + public String getReadFrom() { + return this.readFrom; + } + public Pool getPool() { return this.pool; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 44ba87e00956..eab0b5ab7a06 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2924,6 +2924,52 @@ } ] }, + { + "name": "spring.data.redis.lettuce.read-from", + "values": [ + { + "value": "any", + "description": "Read from any node." + }, + { + "value": "any-replica", + "description": "Read from any replica node." + }, + { + "value": "lowest-latency", + "description": "Read from the node with the lowest latency during topology discovery." + }, + { + "value": "regex:", + "description": "Read from any node that has RedisURI matching with the given pattern." + }, + { + "value": "replica", + "description": "Read from the replica only." + }, + { + "value": "replica-preferred", + "description": "Read preferred from replica and fall back to upstream if no replica is available." + }, + { + "value": "subnet:", + "description": "Read from any node in the subnets." + }, + { + "value": "upstream", + "description": "Read from the upstream only." + }, + { + "value": "upstream-preferred", + "description": "Read preferred from the upstream and fall back to a replica if the upstream is not available." + } + ], + "providers": [ + { + "name": "any" + } + ] + }, { "name": "spring.datasource.data", "providers": [ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index 4e0802ff5893..d27392b2f25a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -19,20 +19,30 @@ import java.time.Duration; import java.util.Arrays; import java.util.EnumSet; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.lettuce.core.ClientOptions; +import io.lettuce.core.ReadFrom; +import io.lettuce.core.ReadFrom.Nodes; +import io.lettuce.core.RedisURI; import io.lettuce.core.cluster.ClusterClientOptions; import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger; +import io.lettuce.core.cluster.models.partitions.RedisClusterNode; +import io.lettuce.core.models.role.RedisNodeDescription; import io.lettuce.core.resource.DefaultClientResources; import io.lettuce.core.tracing.Tracing; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; @@ -112,6 +122,52 @@ void testOverrideRedisConfiguration() { }); } + @ParameterizedTest + @MethodSource + void shouldConfigureLettuceReadFromProperty(String type, ReadFrom readFrom) { + this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:" + type).run((context) -> { + LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class); + LettuceClientConfiguration configuration = factory.getClientConfiguration(); + assertThat(configuration.getReadFrom()).hasValue(readFrom); + }); + } + + @Test + void shouldConfigureLettuceRegexReadFromProperty() { + RedisClusterNode node1 = createRedisNode("redis-node-1.region-1.example.com"); + RedisClusterNode node2 = createRedisNode("redis-node-2.region-1.example.com"); + RedisClusterNode node3 = createRedisNode("redis-node-1.region-2.example.com"); + RedisClusterNode node4 = createRedisNode("redis-node-2.region-2.example.com"); + this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:regex:.*region-1.*") + .run((context) -> { + LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class); + LettuceClientConfiguration configuration = factory.getClientConfiguration(); + assertThat(configuration.getReadFrom()).hasValueSatisfying((readFrom) -> { + List result = readFrom.select(new RedisNodes(node1, node2, node3, node4)); + assertThat(result).hasSize(2).containsExactly(node1, node2); + }); + }); + } + + @Test + void shouldConfigureLettuceSubnetReadFromProperty() { + RedisClusterNode nodeInSubnetIpv4 = createRedisNode("192.0.2.1"); + RedisClusterNode nodeNotInSubnetIpv4 = createRedisNode("198.51.100.1"); + RedisClusterNode nodeInSubnetIpv6 = createRedisNode("2001:db8:abcd:0000::1"); + RedisClusterNode nodeNotInSubnetIpv6 = createRedisNode("2001:db8:abcd:1000::"); + this.contextRunner + .withPropertyValues("spring.data.redis.lettuce.read-from:subnet:192.0.2.0/24,2001:db8:abcd:0000::/52") + .run((context) -> { + LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class); + LettuceClientConfiguration configuration = factory.getClientConfiguration(); + assertThat(configuration.getReadFrom()).hasValueSatisfying((readFrom) -> { + List result = readFrom.select(new RedisNodes(nodeInSubnetIpv4, + nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6)); + assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6); + }); + }); + } + @Test void testCustomizeClientResources() { Tracing tracing = mock(Tracing.class); @@ -632,6 +688,40 @@ private String getUserName(LettuceConnectionFactory factory) { return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername"); } + static Stream shouldConfigureLettuceReadFromProperty() { + return Stream.of(Arguments.of("any", ReadFrom.ANY), Arguments.of("any-replica", ReadFrom.ANY_REPLICA), + Arguments.of("lowest-latency", ReadFrom.LOWEST_LATENCY), Arguments.of("replica", ReadFrom.REPLICA), + Arguments.of("replica-preferred", ReadFrom.REPLICA_PREFERRED), + Arguments.of("upstream", ReadFrom.UPSTREAM), + Arguments.of("upstream-preferred", ReadFrom.UPSTREAM_PREFERRED)); + } + + private RedisClusterNode createRedisNode(String host) { + RedisClusterNode node = new RedisClusterNode(); + node.setUri(RedisURI.Builder.redis(host).build()); + return node; + } + + private static final class RedisNodes implements Nodes { + + private final List descriptions; + + RedisNodes(RedisNodeDescription... descriptions) { + this.descriptions = List.of(descriptions); + } + + @Override + public List getNodes() { + return this.descriptions; + } + + @Override + public Iterator iterator() { + return this.descriptions.iterator(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomConfiguration { From a2a2798a25ad21f1fcfd557754be568ab9520c30 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 18 Oct 2024 13:35:25 -0700 Subject: [PATCH 3/3] Polish 'Add spring.data.redis.lettuce.read-from property' --- .../redis/LettuceConnectionConfiguration.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index c47bf7cf72f9..5d2bf8b83353 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -174,17 +174,25 @@ private void applyProperties(LettuceClientConfiguration.LettuceClientConfigurati } } - private static ReadFrom getReadFrom(String readFrom) { + private ReadFrom getReadFrom(String readFrom) { int index = readFrom.indexOf(':'); if (index == -1) { - String name = readFrom.replaceAll("-", ""); - return ReadFrom.valueOf(name); + return ReadFrom.valueOf(getCanonicalReadFromName(readFrom)); } - String name = readFrom.substring(0, index).replaceAll("-", ""); + String name = getCanonicalReadFromName(readFrom.substring(0, index)); String value = readFrom.substring(index + 1); return ReadFrom.valueOf(name + ":" + value); } + private String getCanonicalReadFromName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } + private ClientOptions createClientOptions( ObjectProvider clientConfigurationBuilderCustomizers) { ClientOptions.Builder builder = initializeClientOptionsBuilder();