diff --git a/src/main/java/io/lettuce/core/ReadFrom.java b/src/main/java/io/lettuce/core/ReadFrom.java index f247fac947..90f06673c6 100644 --- a/src/main/java/io/lettuce/core/ReadFrom.java +++ b/src/main/java/io/lettuce/core/ReadFrom.java @@ -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; @@ -181,9 +182,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}, {@code REGEX:.*region-1.*} and + * {@code ORDER_SENSITIVE_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. */ @@ -193,50 +196,65 @@ public static ReadFrom valueOf(String name) { throw new IllegalArgumentException("Name must not be empty"); } - if (name.equalsIgnoreCase("master")) { + int index = name.indexOf(':'); + + if (index != -1) { + String type = name.substring(0, index).replaceAll("_", ""); + 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") || type.equalsIgnoreCase("ordersensitiveregex")) { + try { + boolean orderSensitive = type.equalsIgnoreCase("ordersensitiveregex"); + return regex(Pattern.compile(value), orderSensitive); + } catch (PatternSyntaxException ex) { + throw new IllegalArgumentException("Value '" + value + "' is not a valid regular expression", ex); + } + } + } + + String type = name.replaceAll("_", ""); + + if (type.equalsIgnoreCase("master")) { return UPSTREAM; } - if (name.equalsIgnoreCase("masterPreferred")) { + if (type.equalsIgnoreCase("masterpreferred")) { return UPSTREAM_PREFERRED; } - if (name.equalsIgnoreCase("upstream")) { + if (type.equalsIgnoreCase("upstream")) { return UPSTREAM; } - if (name.equalsIgnoreCase("upstreamPreferred")) { + if (type.equalsIgnoreCase("upstreampreferred")) { return UPSTREAM_PREFERRED; } - if (name.equalsIgnoreCase("slave") || name.equalsIgnoreCase("replica")) { + if (type.equalsIgnoreCase("slave") || type.equalsIgnoreCase("replica")) { return REPLICA; } - if (name.equalsIgnoreCase("slavePreferred") || name.equalsIgnoreCase("replicaPreferred")) { + if (type.equalsIgnoreCase("slavepreferred") || type.equalsIgnoreCase("replicapreferred")) { return REPLICA_PREFERRED; } - if (name.equalsIgnoreCase("nearest") || name.equalsIgnoreCase("lowestLatency")) { + if (type.equalsIgnoreCase("nearest") || type.equalsIgnoreCase("lowestlatency")) { return LOWEST_LATENCY; } - if (name.equalsIgnoreCase("any")) { + if (type.equalsIgnoreCase("any")) { return ANY; } - if (name.equalsIgnoreCase("anyReplica")) { + if (type.equalsIgnoreCase("anyreplica")) { 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"); } diff --git a/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java b/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java index 3ee0b59450..7de5e29051 100644 --- a/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java @@ -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; @@ -220,44 +222,149 @@ 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); + } + + @ParameterizedTest + @ValueSource(strings = { "MASTER_PREFERRED", "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 = { "SLAVE_PREFERRED", "slavePreferred", "slavepreferred" }) + void valueOfSlavePreferred(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED); + } + + @ParameterizedTest + @ValueSource(strings = { "REPLICA_PREFERRED", "replicaPreferred", "replicapreferred" }) + void valueOfReplicaPreferred(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 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 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 result = sut.select(getNodes(node1, node2, node3, node4)); + + assertThat(sut).hasFieldOrPropertyWithValue("orderSensitive", false); + assertThat(result).hasSize(2).containsExactly(node1, node2); + } + + @ParameterizedTest + @ValueSource(strings = { "orderSensitiveRegex:.*region-1.*", "ORDER_SENSITIVE_REGEX:.*region-1.*" }) + void valueOfOrderSensitiveRegex(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 result = sut.select(getNodes(node1, node2, node3, node4)); + + assertThat(sut).hasFieldOrPropertyWithValue("orderSensitive", true); + 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); } @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() {