Merge pull request #42588 from nosan
* pr/42588: Polish "Add spring.data.redis.lettuce.read-from property" Add spring.data.redis.lettuce.read-from property Closes gh-42588
This commit is contained in:
commit
db8e1640a1
|
@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.data.redis;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import io.lettuce.core.ClientOptions;
|
import io.lettuce.core.ClientOptions;
|
||||||
|
import io.lettuce.core.ReadFrom;
|
||||||
import io.lettuce.core.RedisClient;
|
import io.lettuce.core.RedisClient;
|
||||||
import io.lettuce.core.SocketOptions;
|
import io.lettuce.core.SocketOptions;
|
||||||
import io.lettuce.core.TimeoutOptions;
|
import io.lettuce.core.TimeoutOptions;
|
||||||
|
@ -163,12 +164,35 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
|
||||||
if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
|
if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
|
||||||
builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout());
|
builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout());
|
||||||
}
|
}
|
||||||
|
String readFrom = lettuce.getReadFrom();
|
||||||
|
if (readFrom != null) {
|
||||||
|
builder.readFrom(getReadFrom(readFrom));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(getProperties().getClientName())) {
|
if (StringUtils.hasText(getProperties().getClientName())) {
|
||||||
builder.clientName(getProperties().getClientName());
|
builder.clientName(getProperties().getClientName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ReadFrom getReadFrom(String readFrom) {
|
||||||
|
int index = readFrom.indexOf(':');
|
||||||
|
if (index == -1) {
|
||||||
|
return ReadFrom.valueOf(getCanonicalReadFromName(readFrom));
|
||||||
|
}
|
||||||
|
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(
|
private ClientOptions createClientOptions(
|
||||||
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientConfigurationBuilderCustomizers) {
|
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientConfigurationBuilderCustomizers) {
|
||||||
ClientOptions.Builder builder = initializeClientOptionsBuilder();
|
ClientOptions.Builder builder = initializeClientOptionsBuilder();
|
||||||
|
|
|
@ -467,6 +467,11 @@ public class RedisProperties {
|
||||||
*/
|
*/
|
||||||
private Duration shutdownTimeout = Duration.ofMillis(100);
|
private Duration shutdownTimeout = Duration.ofMillis(100);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines from which Redis nodes data is read.
|
||||||
|
*/
|
||||||
|
private String readFrom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lettuce pool configuration.
|
* Lettuce pool configuration.
|
||||||
*/
|
*/
|
||||||
|
@ -482,6 +487,14 @@ public class RedisProperties {
|
||||||
this.shutdownTimeout = shutdownTimeout;
|
this.shutdownTimeout = shutdownTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setReadFrom(String readFrom) {
|
||||||
|
this.readFrom = readFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReadFrom() {
|
||||||
|
return this.readFrom;
|
||||||
|
}
|
||||||
|
|
||||||
public Pool getPool() {
|
public Pool getPool() {
|
||||||
return this.pool;
|
return this.pool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
"name": "spring.datasource.data",
|
||||||
"providers": [
|
"providers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2024 the original author or authors.
|
* Copyright 2012-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -19,20 +19,30 @@ package org.springframework.boot.autoconfigure.data.redis;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import io.lettuce.core.ClientOptions;
|
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.ClusterClientOptions;
|
||||||
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger;
|
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.resource.DefaultClientResources;
|
||||||
import io.lettuce.core.tracing.Tracing;
|
import io.lettuce.core.tracing.Tracing;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||||
import org.junit.jupiter.api.condition.JRE;
|
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.AutoConfigurations;
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool;
|
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool;
|
||||||
|
@ -112,6 +122,60 @@ class RedisAutoConfigurationTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<RedisNodeDescription> 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<RedisNodeDescription> result = readFrom.select(new RedisNodes(nodeInSubnetIpv4,
|
||||||
|
nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
|
||||||
|
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCustomizeClientResources() {
|
void testCustomizeClientResources() {
|
||||||
Tracing tracing = mock(Tracing.class);
|
Tracing tracing = mock(Tracing.class);
|
||||||
|
@ -632,6 +696,32 @@ class RedisAutoConfigurationTests {
|
||||||
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
|
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<RedisNodeDescription> descriptions;
|
||||||
|
|
||||||
|
RedisNodes(RedisNodeDescription... descriptions) {
|
||||||
|
this.descriptions = List.of(descriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RedisNodeDescription> getNodes() {
|
||||||
|
return this.descriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<RedisNodeDescription> iterator() {
|
||||||
|
return this.descriptions.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class CustomConfiguration {
|
static class CustomConfiguration {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue