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 (#3016)

Before this commit, it was not possible to use ReadFrom.valueOf(name) for subnet and regex types.
This commit introduces a new syntax subnet:192.168.0.0/16,2001:db8:abcd:0000::/52 and regex:.*region-1.* respectively
  • Loading branch information
nosan authored Oct 18, 2024
1 parent d255b1a commit 8fc5a3f
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 29 deletions.
33 changes: 23 additions & 10 deletions src/main/java/io/lettuce/core/ReadFrom.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import io.lettuce.core.internal.LettuceStrings;
import io.lettuce.core.models.role.RedisNodeDescription;
Expand Down Expand Up @@ -181,9 +182,10 @@ 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 case-insensitive name of the read from setting
* @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,25 @@ 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")) {
try {
return regex(Pattern.compile(value));
} catch (PatternSyntaxException ex) {
throw new IllegalArgumentException("Value '" + value + "' is not a valid regular expression", ex);
}
}
}

if (name.equalsIgnoreCase("master")) {
return UPSTREAM;
}
Expand Down Expand Up @@ -229,14 +250,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
129 changes: 110 additions & 19 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,44 +222,133 @@ 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", "lowestlatency", "LOWESTLATENCY" })
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);
}

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

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

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

@ParameterizedTest
@ValueSource(strings = { "replicaPreferred", "replicapreferred", "REPLICAPREFERRED" })
void valueOfReplicaPreferred(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
}

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

@Test
void valueOfSlave() {
assertThat(ReadFrom.valueOf("slave")).isEqualTo(ReadFrom.REPLICA);
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 valueOfSlavePreferred() {
assertThat(ReadFrom.valueOf("slavePreferred")).isEqualTo(ReadFrom.REPLICA_PREFERRED);
void valueOfRegexWithEmptyRegexValue() {
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
}

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

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(sut).hasFieldOrPropertyWithValue("orderSensitive", false);
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" })
void valueOfUpstream(String name) {
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
}

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

@Test
void valueOfAnyReplica() {
assertThat(ReadFrom.valueOf("anyReplica")).isEqualTo(ReadFrom.ANY_REPLICA);
void valueOfWhenNameIsPresentButValueIsAbsent() {
assertThatThrownBy(() -> ReadFrom.valueOf("subnet:")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value must not be empty for the type 'subnet'");
}

@Test
void valueOfSubnet() {
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
void valueOfWhenNameIsEmptyButValueIsPresent() {
assertThatThrownBy(() -> ReadFrom.valueOf(":192.0.2.0/24")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("ReadFrom :192.0.2.0/24 not supported");
}

@Test
void valueOfRegex() {
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
void valueOfRegexWithInvalidPatternShouldThrownIllegalArgumentException() {
assertThatThrownBy(() -> ReadFrom.valueOf("regex:\\")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("is not a valid regular expression");
}

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

private ReadFrom.Nodes getNodes() {
Expand Down

0 comments on commit 8fc5a3f

Please sign in to comment.