Skip to content

Commit

Permalink
Add support for creating regex and subnet-based ReadFrom instances fr…
Browse files Browse the repository at this point in the history
…om a single string #3013

Before this commit, it was not possible to use ReadFrom.valueOf for subnet and regex types.
This commit introduces support for these types, as well as the use of names in underscore format
  • Loading branch information
nosan committed Oct 15, 2024
1 parent d255b1a commit ebff5d4
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 30 deletions.
30 changes: 20 additions & 10 deletions src/main/java/io/lettuce/core/ReadFrom.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,11 @@ protected boolean isOrderSensitive() {
}

/**
* Retrieve the {@link ReadFrom} preset by name.
* Retrieve the {@link ReadFrom} preset by name. For complex types like {@code subnet} or {@code regex}, the following
* syntax could be used {@code subnet:192.168.0.0/16,2001:db8:abcd:0000::/52} and {@code regex:.*region-1.*} respectively.
*
* @param name the name of the read from setting
* @param name the name of the read from setting (in different formats like {@code UPSTREAM_PREFERRED},
* {@code upstreamPreferred}).
* @return the {@link ReadFrom} preset
* @throws IllegalArgumentException if {@code name} is empty, {@code null} or the {@link ReadFrom} preset is unknown.
*/
Expand All @@ -193,6 +195,22 @@ public static ReadFrom valueOf(String name) {
throw new IllegalArgumentException("Name must not be empty");
}

int index = name.indexOf(':');

if (index != -1) {
String type = name.substring(0, index);
String value = name.substring(index + 1);
if (LettuceStrings.isEmpty(value)) {
throw new IllegalArgumentException("Value must not be empty for the type '" + type + "'");
}
if (type.equalsIgnoreCase("subnet")) {
return subnet(value.split(","));
}
if (type.equalsIgnoreCase("regex")) {
return regex(Pattern.compile(value));
}
}
name = name.replaceAll("_", "");
if (name.equalsIgnoreCase("master")) {
return UPSTREAM;
}
Expand Down Expand Up @@ -229,14 +247,6 @@ public static ReadFrom valueOf(String name) {
return ANY_REPLICA;
}

if (name.equalsIgnoreCase("subnet")) {
throw new IllegalArgumentException("subnet must be created via ReadFrom#subnet");
}

if (name.equalsIgnoreCase("regex")) {
throw new IllegalArgumentException("regex must be created via ReadFrom#regex");
}

throw new IllegalArgumentException("ReadFrom " + name + " not supported");
}

Expand Down
106 changes: 86 additions & 20 deletions src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import io.lettuce.core.ReadFrom;
import io.lettuce.core.RedisURI;
Expand Down Expand Up @@ -220,46 +222,110 @@ void valueOfUnknown() {
assertThatThrownBy(() -> ReadFrom.valueOf("unknown")).isInstanceOf(IllegalArgumentException.class);
}

@Test
void valueOfNearest() {
assertThat(ReadFrom.valueOf("nearest")).isEqualTo(ReadFrom.NEAREST);
@ParameterizedTest
@ValueSource(strings = { "NEAREST", "nearest", "NeareSt" })
void valueOfNearest(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.NEAREST);
}

@Test
void valueOfMaster() {
assertThat(ReadFrom.valueOf("master")).isEqualTo(ReadFrom.UPSTREAM);
@ParameterizedTest
@ValueSource(strings = { "lowestLatency", "LOWEST_LATENCY" })
void valueOfLowestLatency(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.LOWEST_LATENCY);
}

@Test
void valueOfMasterPreferred() {
assertThat(ReadFrom.valueOf("masterPreferred")).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
@ParameterizedTest
@ValueSource(strings = { "MASTER", "master", "MasTeR" })
void valueOfMaster(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
}

@Test
void valueOfSlave() {
assertThat(ReadFrom.valueOf("slave")).isEqualTo(ReadFrom.REPLICA);
@ParameterizedTest
@ValueSource(strings = { "MASTER_PREFERRED", "masterPreferred", "masterpreferred" })
void valueOfMasterPreferred(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
}

@Test
void valueOfSlavePreferred() {
assertThat(ReadFrom.valueOf("slavePreferred")).isEqualTo(ReadFrom.REPLICA_PREFERRED);
@ParameterizedTest
@ValueSource(strings = { "slave", "SLAVE", "sLave" })
void valueOfSlave(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
}

@Test
void valueOfAnyReplica() {
assertThat(ReadFrom.valueOf("anyReplica")).isEqualTo(ReadFrom.ANY_REPLICA);
@ParameterizedTest
@ValueSource(strings = { "SLAVE_PREFERRED", "slavePreferred", "slavepreferred" })
void valueOfSlavePreferred(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
}

@ParameterizedTest
@ValueSource(strings = { "ANY_REPLICA", "anyReplica", "anyreplica" })
void valueOfAnyReplica(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY_REPLICA);
}

@Test
void valueOfSubnet() {
void valueOfSubnetWithEmptyCidrNotations() {
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
}

@ParameterizedTest
@ValueSource(strings = { "subnet:192.0.2.0/24,2001:db8:abcd:0000::/52", "SUBNET:192.0.2.0/24,2001:db8:abcd:0000::/52" })
void valueOfSubnet(String name) {
RedisClusterNode nodeInSubnetIpv4 = createNodeWithHost("192.0.2.1");
RedisClusterNode nodeNotInSubnetIpv4 = createNodeWithHost("198.51.100.1");
RedisClusterNode nodeInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:0000::1");
RedisClusterNode nodeNotInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:1000::");
ReadFrom sut = ReadFrom.valueOf(name);
List<RedisNodeDescription> result = sut
.select(getNodes(nodeInSubnetIpv4, nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
}

@Test
void valueOfRegex() {
void valueOfRegexWithEmptyRegexValue() {
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
}

@ParameterizedTest
@ValueSource(strings = { "regex:.*region-1.*", "REGEX:.*region-1.*" })
void valueOfRegex() {
ReadFrom sut = ReadFrom.valueOf("regex:.*region-1.*");

RedisClusterNode node1 = createNodeWithHost("redis-node-1.region-1.example.com");
RedisClusterNode node2 = createNodeWithHost("redis-node-2.region-1.example.com");
RedisClusterNode node3 = createNodeWithHost("redis-node-1.region-2.example.com");
RedisClusterNode node4 = createNodeWithHost("redis-node-2.region-2.example.com");

List<RedisNodeDescription> result = sut.select(getNodes(node1, node2, node3, node4));

assertThat(result).hasSize(2).containsExactly(node1, node2);
}

@ParameterizedTest
@ValueSource(strings = { "replica", "Replica", "REPLICA" })
void valueOfReplica(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
}

@ParameterizedTest
@ValueSource(strings = { "UPSTREAM", "Upstream", "upstream", "UpStream" })
void valueOfUpstream(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
}

@ParameterizedTest
@ValueSource(strings = { "UPSTREAM_PREFERRED", "upstreamPreferred", "UpStreamPreferred" })
void valueOfUpstreamPreferred(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
}

@ParameterizedTest
@ValueSource(strings = { "ANY", "any", "Any" })
void valueOfAny(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY);
}

private ReadFrom.Nodes getNodes() {
return new ReadFrom.Nodes() {

Expand Down

0 comments on commit ebff5d4

Please sign in to comment.