Skip to content

Commit

Permalink
Add spring.data.redis.lettuce.read-from property
Browse files Browse the repository at this point in the history
  • Loading branch information
nosan committed Oct 18, 2024
1 parent 5321d46 commit b42429c
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 0 deletions.
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,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<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,31 @@
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.Disabled;
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 +123,53 @@ void testOverrideRedisConfiguration() {
});
}

@ParameterizedTest
@MethodSource
void shouldConfigureLettuceReadFromProperty(String type, ReadFrom readFrom) {
this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:" + type).run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getClientConfiguration().getReadFrom()).hasValue(readFrom);
});
}

@Test
@Disabled("Lettuce 6.5.0.RELEASE")
void shouldConfigureLettuceReadFromPropertyRegexType() {
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
@Disabled("Lettuce 6.5.0.RELEASE")
void shouldConfigureLettuceReadFromPropertySubnetType() {
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 +690,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

0 comments on commit b42429c

Please sign in to comment.