Skip to content

Add spring.data.redis.lettuce.read-from property #42588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -163,12 +164,35 @@ 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 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(
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientConfigurationBuilderCustomizers) {
ClientOptions.Builder builder = initializeClientOptionsBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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
void testCustomizeClientResources() {
Tracing tracing = mock(Tracing.class);
Expand Down Expand Up @@ -632,6 +688,40 @@ private String getUserName(LettuceConnectionFactory factory) {
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
}

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

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)
static class CustomConfiguration {

Expand Down
2 changes: 1 addition & 1 deletion spring-boot-project/spring-boot-dependencies/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down