Skip to content

Commit 17f9b1f

Browse files
committed
Add support for creating regex and subnet-based ReadFrom instances from 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
1 parent d255b1a commit 17f9b1f

File tree

2 files changed

+163
-38
lines changed

2 files changed

+163
-38
lines changed

src/main/java/io/lettuce/core/ReadFrom.java

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.util.List;
2323
import java.util.regex.Pattern;
24+
import java.util.regex.PatternSyntaxException;
2425

2526
import io.lettuce.core.internal.LettuceStrings;
2627
import io.lettuce.core.models.role.RedisNodeDescription;
@@ -181,9 +182,11 @@ protected boolean isOrderSensitive() {
181182
}
182183

183184
/**
184-
* Retrieve the {@link ReadFrom} preset by name.
185+
* Retrieve the {@link ReadFrom} preset by name. For complex types like {@code SUBNET} or {@code REGEX}, the following
186+
* syntax could be used {@code SUBNET:192.168.0.0/16,2001:db8:abcd:0000::/52}, {@code REGEX:.*region-1.*} and
187+
* {@code ORDER_SENSITIVE_REGEX:.*region-1.*} respectively.
185188
*
186-
* @param name the name of the read from setting
189+
* @param name the case-insensitive name of the read from setting
187190
* @return the {@link ReadFrom} preset
188191
* @throws IllegalArgumentException if {@code name} is empty, {@code null} or the {@link ReadFrom} preset is unknown.
189192
*/
@@ -193,50 +196,65 @@ public static ReadFrom valueOf(String name) {
193196
throw new IllegalArgumentException("Name must not be empty");
194197
}
195198

196-
if (name.equalsIgnoreCase("master")) {
199+
int index = name.indexOf(':');
200+
201+
if (index != -1) {
202+
String type = name.substring(0, index).replaceAll("_", "");
203+
String value = name.substring(index + 1);
204+
if (LettuceStrings.isEmpty(value)) {
205+
throw new IllegalArgumentException("Value must not be empty for the type '" + type + "'");
206+
}
207+
if (type.equalsIgnoreCase("subnet")) {
208+
return subnet(value.split(","));
209+
}
210+
if (type.equalsIgnoreCase("regex") || type.equalsIgnoreCase("ordersensitiveregex")) {
211+
try {
212+
boolean orderSensitive = type.equalsIgnoreCase("ordersensitiveregex");
213+
return regex(Pattern.compile(value), orderSensitive);
214+
} catch (PatternSyntaxException ex) {
215+
throw new IllegalArgumentException("Value '" + value + "' is not a valid regular expression", ex);
216+
}
217+
}
218+
}
219+
220+
String type = name.replaceAll("_", "");
221+
222+
if (type.equalsIgnoreCase("master")) {
197223
return UPSTREAM;
198224
}
199225

200-
if (name.equalsIgnoreCase("masterPreferred")) {
226+
if (type.equalsIgnoreCase("masterpreferred")) {
201227
return UPSTREAM_PREFERRED;
202228
}
203229

204-
if (name.equalsIgnoreCase("upstream")) {
230+
if (type.equalsIgnoreCase("upstream")) {
205231
return UPSTREAM;
206232
}
207233

208-
if (name.equalsIgnoreCase("upstreamPreferred")) {
234+
if (type.equalsIgnoreCase("upstreampreferred")) {
209235
return UPSTREAM_PREFERRED;
210236
}
211237

212-
if (name.equalsIgnoreCase("slave") || name.equalsIgnoreCase("replica")) {
238+
if (type.equalsIgnoreCase("slave") || type.equalsIgnoreCase("replica")) {
213239
return REPLICA;
214240
}
215241

216-
if (name.equalsIgnoreCase("slavePreferred") || name.equalsIgnoreCase("replicaPreferred")) {
242+
if (type.equalsIgnoreCase("slavepreferred") || type.equalsIgnoreCase("replicapreferred")) {
217243
return REPLICA_PREFERRED;
218244
}
219245

220-
if (name.equalsIgnoreCase("nearest") || name.equalsIgnoreCase("lowestLatency")) {
246+
if (type.equalsIgnoreCase("nearest") || type.equalsIgnoreCase("lowestlatency")) {
221247
return LOWEST_LATENCY;
222248
}
223249

224-
if (name.equalsIgnoreCase("any")) {
250+
if (type.equalsIgnoreCase("any")) {
225251
return ANY;
226252
}
227253

228-
if (name.equalsIgnoreCase("anyReplica")) {
254+
if (type.equalsIgnoreCase("anyreplica")) {
229255
return ANY_REPLICA;
230256
}
231257

232-
if (name.equalsIgnoreCase("subnet")) {
233-
throw new IllegalArgumentException("subnet must be created via ReadFrom#subnet");
234-
}
235-
236-
if (name.equalsIgnoreCase("regex")) {
237-
throw new IllegalArgumentException("regex must be created via ReadFrom#regex");
238-
}
239-
240258
throw new IllegalArgumentException("ReadFrom " + name + " not supported");
241259
}
242260

src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java

Lines changed: 126 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
import org.junit.jupiter.api.BeforeEach;
3333
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.ValueSource;
3436

3537
import io.lettuce.core.ReadFrom;
3638
import io.lettuce.core.RedisURI;
@@ -220,44 +222,149 @@ void valueOfUnknown() {
220222
assertThatThrownBy(() -> ReadFrom.valueOf("unknown")).isInstanceOf(IllegalArgumentException.class);
221223
}
222224

223-
@Test
224-
void valueOfNearest() {
225-
assertThat(ReadFrom.valueOf("nearest")).isEqualTo(ReadFrom.NEAREST);
225+
@ParameterizedTest
226+
@ValueSource(strings = { "NEAREST", "nearest", "NeareSt" })
227+
void valueOfNearest(String name) {
228+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.NEAREST);
226229
}
227230

228-
@Test
229-
void valueOfMaster() {
230-
assertThat(ReadFrom.valueOf("master")).isEqualTo(ReadFrom.UPSTREAM);
231+
@ParameterizedTest
232+
@ValueSource(strings = { "lowestLatency", "LOWEST_LATENCY" })
233+
void valueOfLowestLatency(String name) {
234+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.LOWEST_LATENCY);
231235
}
232236

233-
@Test
234-
void valueOfMasterPreferred() {
235-
assertThat(ReadFrom.valueOf("masterPreferred")).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
237+
@ParameterizedTest
238+
@ValueSource(strings = { "MASTER", "master", "MasTeR" })
239+
void valueOfMaster(String name) {
240+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
241+
}
242+
243+
@ParameterizedTest
244+
@ValueSource(strings = { "MASTER_PREFERRED", "masterPreferred", "masterpreferred" })
245+
void valueOfMasterPreferred(String name) {
246+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
247+
}
248+
249+
@ParameterizedTest
250+
@ValueSource(strings = { "slave", "SLAVE", "sLave" })
251+
void valueOfSlave(String name) {
252+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
253+
}
254+
255+
@ParameterizedTest
256+
@ValueSource(strings = { "SLAVE_PREFERRED", "slavePreferred", "slavepreferred" })
257+
void valueOfSlavePreferred(String name) {
258+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
259+
}
260+
261+
@ParameterizedTest
262+
@ValueSource(strings = { "REPLICA_PREFERRED", "replicaPreferred", "replicapreferred" })
263+
void valueOfReplicaPreferred(String name) {
264+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED);
265+
}
266+
267+
@ParameterizedTest
268+
@ValueSource(strings = { "ANY_REPLICA", "anyReplica", "anyreplica" })
269+
void valueOfAnyReplica(String name) {
270+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY_REPLICA);
236271
}
237272

238273
@Test
239-
void valueOfSlave() {
240-
assertThat(ReadFrom.valueOf("slave")).isEqualTo(ReadFrom.REPLICA);
274+
void valueOfSubnetWithEmptyCidrNotations() {
275+
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
276+
}
277+
278+
@ParameterizedTest
279+
@ValueSource(strings = { "subnet:192.0.2.0/24,2001:db8:abcd:0000::/52", "SUBNET:192.0.2.0/24,2001:db8:abcd:0000::/52" })
280+
void valueOfSubnet(String name) {
281+
RedisClusterNode nodeInSubnetIpv4 = createNodeWithHost("192.0.2.1");
282+
RedisClusterNode nodeNotInSubnetIpv4 = createNodeWithHost("198.51.100.1");
283+
RedisClusterNode nodeInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:0000::1");
284+
RedisClusterNode nodeNotInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:1000::");
285+
ReadFrom sut = ReadFrom.valueOf(name);
286+
List<RedisNodeDescription> result = sut
287+
.select(getNodes(nodeInSubnetIpv4, nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
288+
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
241289
}
242290

243291
@Test
244-
void valueOfSlavePreferred() {
245-
assertThat(ReadFrom.valueOf("slavePreferred")).isEqualTo(ReadFrom.REPLICA_PREFERRED);
292+
void valueOfRegexWithEmptyRegexValue() {
293+
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
294+
}
295+
296+
@ParameterizedTest
297+
@ValueSource(strings = { "regex:.*region-1.*", "REGEX:.*region-1.*" })
298+
void valueOfRegex(String name) {
299+
ReadFrom sut = ReadFrom.valueOf(name);
300+
301+
RedisClusterNode node1 = createNodeWithHost("redis-node-1.region-1.example.com");
302+
RedisClusterNode node2 = createNodeWithHost("redis-node-2.region-1.example.com");
303+
RedisClusterNode node3 = createNodeWithHost("redis-node-1.region-2.example.com");
304+
RedisClusterNode node4 = createNodeWithHost("redis-node-2.region-2.example.com");
305+
306+
List<RedisNodeDescription> result = sut.select(getNodes(node1, node2, node3, node4));
307+
308+
assertThat(sut).hasFieldOrPropertyWithValue("orderSensitive", false);
309+
assertThat(result).hasSize(2).containsExactly(node1, node2);
310+
}
311+
312+
@ParameterizedTest
313+
@ValueSource(strings = { "orderSensitiveRegex:.*region-1.*", "ORDER_SENSITIVE_REGEX:.*region-1.*" })
314+
void valueOfOrderSensitiveRegex(String name) {
315+
ReadFrom sut = ReadFrom.valueOf(name);
316+
317+
RedisClusterNode node1 = createNodeWithHost("redis-node-1.region-1.example.com");
318+
RedisClusterNode node2 = createNodeWithHost("redis-node-2.region-1.example.com");
319+
RedisClusterNode node3 = createNodeWithHost("redis-node-1.region-2.example.com");
320+
RedisClusterNode node4 = createNodeWithHost("redis-node-2.region-2.example.com");
321+
322+
List<RedisNodeDescription> result = sut.select(getNodes(node1, node2, node3, node4));
323+
324+
assertThat(sut).hasFieldOrPropertyWithValue("orderSensitive", true);
325+
assertThat(result).hasSize(2).containsExactly(node1, node2);
326+
}
327+
328+
@ParameterizedTest
329+
@ValueSource(strings = { "replica", "Replica", "REPLICA" })
330+
void valueOfReplica(String name) {
331+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA);
332+
}
333+
334+
@ParameterizedTest
335+
@ValueSource(strings = { "UPSTREAM", "Upstream", "upstream", "UpStream" })
336+
void valueOfUpstream(String name) {
337+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM);
338+
}
339+
340+
@ParameterizedTest
341+
@ValueSource(strings = { "UPSTREAM_PREFERRED", "upstreamPreferred", "UpStreamPreferred" })
342+
void valueOfUpstreamPreferred(String name) {
343+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED);
246344
}
247345

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

253352
@Test
254-
void valueOfSubnet() {
255-
assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class);
353+
void valueOfWhenNameIsEmptyButValueIsPresent() {
354+
assertThatThrownBy(() -> ReadFrom.valueOf(":192.0.2.0/24")).isInstanceOf(IllegalArgumentException.class)
355+
.hasMessageContaining("ReadFrom :192.0.2.0/24 not supported");
256356
}
257357

258358
@Test
259-
void valueOfRegex() {
260-
assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class);
359+
void valueOfRegexWithInvalidPatternShouldThrownIllegalArgumentException() {
360+
assertThatThrownBy(() -> ReadFrom.valueOf("regex:\\")).isInstanceOf(IllegalArgumentException.class)
361+
.hasMessageContaining("is not a valid regular expression");
362+
}
363+
364+
@ParameterizedTest
365+
@ValueSource(strings = { "ANY", "any", "Any" })
366+
void valueOfAny(String name) {
367+
assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY);
261368
}
262369

263370
private ReadFrom.Nodes getNodes() {

0 commit comments

Comments
 (0)