Skip to content

Commit

Permalink
Add cluster client, request routes configuration and support for bulk…
Browse files Browse the repository at this point in the history
… response (#59)

* Add cluster client and routes support for cluster client.

Signed-off-by: Yury-Fridlyand <[email protected]>

* Address PR feedback and add tests.

Signed-off-by: Yury-Fridlyand <[email protected]>

* Minor javadoc update.

Signed-off-by: Yury-Fridlyand <[email protected]>

* Minor javadoc fix

Signed-off-by: Yury-Fridlyand <[email protected]>

* Address PR review comments.

Signed-off-by: Yury-Fridlyand <[email protected]>

* Address PR review comments.

Signed-off-by: Yury-Fridlyand <[email protected]>

* Address PR review comments.

Signed-off-by: Yury-Fridlyand <[email protected]>

---------

Signed-off-by: Yury-Fridlyand <[email protected]>
  • Loading branch information
Yury-Fridlyand authored and acarbonetto committed Feb 1, 2024
1 parent 888109e commit 6d180ba
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 2 deletions.
126 changes: 126 additions & 0 deletions java/client/src/main/java/glide/api/models/configuration/Route.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package glide.api.models.configuration;

import java.util.Optional;
import lombok.Builder;
import lombok.Getter;

/** Request routing configuration. */
public class Route {

public enum RouteType {
/** Route request to all nodes. */
ALL_NODES,
/** Route request to all primary nodes. */
ALL_PRIMARIES,
/** Route request to a random node. */
RANDOM,
/** Route request to the primary node that contains the slot with the given id. */
PRIMARY_SLOT_ID,
/** Route request to the replica node that contains the slot with the given id. */
REPLICA_SLOT_ID,
/** Route request to the primary node that contains the slot that the given key matches. */
PRIMARY_SLOT_KEY,
/** Route request to the replica node that contains the slot that the given key matches. */
REPLICA_SLOT_KEY,
}

@Getter private final RouteType routeType;

private final Optional<Integer> slotId;

private final Optional<String> slotKey;

public Integer getSlotId() {
assert slotId.isPresent();
return slotId.get();
}

public String getSlotKey() {
assert slotKey.isPresent();
return slotKey.get();
}

private Route(RouteType routeType, Integer slotId) {
this.routeType = routeType;
this.slotId = Optional.of(slotId);
this.slotKey = Optional.empty();
}

private Route(RouteType routeType, String slotKey) {
this.routeType = routeType;
this.slotId = Optional.empty();
this.slotKey = Optional.of(slotKey);
}

private Route(RouteType routeType) {
this.routeType = routeType;
this.slotId = Optional.empty();
this.slotKey = Optional.empty();
}

public static class Builder {
private final RouteType routeType;
private int slotId;
private boolean slotIdSet = false;
private String slotKey;
private boolean slotKeySet = false;

/**
* Request routing configuration overrides the {@link ReadFrom} connection configuration.<br>
* If {@link RouteType#REPLICA_SLOT_ID} or {@link RouteType#REPLICA_SLOT_KEY} is used, the
* request will be routed to a replica, even if the strategy is {@link ReadFrom#PRIMARY}.
*/
public Builder(RouteType routeType) {
this.routeType = routeType;
}

/**
* Slot number. There are 16384 slots in a redis cluster, and each shard manages a slot range.
* Unless the slot is known, it's better to route using {@link RouteType#PRIMARY_SLOT_KEY} or
* {@link RouteType#REPLICA_SLOT_KEY}.<br>
* Could be used with {@link RouteType#PRIMARY_SLOT_ID} or {@link RouteType#REPLICA_SLOT_ID}
* only.
*/
public Builder setSlotId(int slotId) {
if (!(routeType == RouteType.PRIMARY_SLOT_ID || routeType == RouteType.REPLICA_SLOT_ID)) {
throw new IllegalArgumentException(
"Slot ID could be set for corresponding types of route only");
}
this.slotId = slotId;
slotIdSet = true;
return this;
}

/**
* The request will be sent to nodes managing this key.<br>
* Could be used with {@link RouteType#PRIMARY_SLOT_KEY} or {@link RouteType#REPLICA_SLOT_KEY}
* only.
*/
public Builder setSlotKey(String slotKey) {
if (!(routeType == RouteType.PRIMARY_SLOT_KEY || routeType == RouteType.REPLICA_SLOT_KEY)) {
throw new IllegalArgumentException(
"Slot key could be set for corresponding types of route only");
}
this.slotKey = slotKey;
slotKeySet = true;
return this;
}

public Route build() {
if (routeType == RouteType.PRIMARY_SLOT_ID || routeType == RouteType.REPLICA_SLOT_ID) {
if (!slotIdSet) {
throw new IllegalArgumentException("Slot ID is missing");
}
return new Route(routeType, slotId);
}
if (routeType == RouteType.PRIMARY_SLOT_KEY || routeType == RouteType.REPLICA_SLOT_KEY) {
if (!slotKeySet) {
throw new IllegalArgumentException("Slot key is missing");
}
return new Route(routeType, slotKey);
}

return new Route(routeType);
}
}
}
4 changes: 2 additions & 2 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void customCommand_success() throws ExecutionException, InterruptedExcept
String cmd = "GETSTRING";
CompletableFuture<Object> testResponse = mock(CompletableFuture.class);
when(testResponse.get()).thenReturn(value);
when(commandManager.submitNewCommand(any(), any())).thenReturn(testResponse);
when(commandManager.submitNewCommand(any(), any(), any())).thenReturn(testResponse);

// exercise
CompletableFuture<Object> response = service.customCommand(new String[] {cmd, key});
Expand All @@ -57,7 +57,7 @@ public void customCommand_interruptedException() throws ExecutionException, Inte
CompletableFuture<Object> testResponse = mock(CompletableFuture.class);
InterruptedException interruptedException = new InterruptedException();
when(testResponse.get()).thenThrow(interruptedException);
when(commandManager.submitNewCommand(any(), any())).thenReturn(testResponse);
when(commandManager.submitNewCommand(any(), any(), any())).thenReturn(testResponse);

// exercise
InterruptedException exception =
Expand Down
93 changes: 93 additions & 0 deletions java/client/src/test/java/glide/api/models/RouteBuilderTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package glide.api.models;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import glide.api.models.configuration.Route;
import glide.api.models.configuration.Route.RouteType;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

public class RouteBuilderTests {

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"PRIMARY_SLOT_ID", "REPLICA_SLOT_ID"})
public void slot_id_is_required(RouteType routeType) {
var exception =
assertThrows(IllegalArgumentException.class, () -> new Route.Builder(routeType).build());
assertEquals("Slot ID is missing", exception.getMessage());
}

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"PRIMARY_SLOT_KEY", "REPLICA_SLOT_KEY"})
public void slot_key_is_required(RouteType routeType) {
var exception =
assertThrows(IllegalArgumentException.class, () -> new Route.Builder(routeType).build());
assertEquals("Slot key is missing", exception.getMessage());
}

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"PRIMARY_SLOT_KEY", "REPLICA_SLOT_KEY", "ALL_NODES", "ALL_PRIMARIES", "RANDOM"})
public void slot_id_not_acceptable(RouteType routeType) {
var exception =
assertThrows(
IllegalArgumentException.class, () -> new Route.Builder(routeType).setSlotId(42));
assertEquals(
"Slot ID could be set for corresponding types of route only", exception.getMessage());
}

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"PRIMARY_SLOT_ID", "REPLICA_SLOT_ID", "ALL_NODES", "ALL_PRIMARIES", "RANDOM"})
public void slot_key_not_acceptable(RouteType routeType) {
var exception =
assertThrows(
IllegalArgumentException.class, () -> new Route.Builder(routeType).setSlotKey("D'oh"));
assertEquals(
"Slot key could be set for corresponding types of route only", exception.getMessage());
}

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"PRIMARY_SLOT_ID", "REPLICA_SLOT_ID"})
public void build_with_slot_id(RouteType routeType) {
var route = new Route.Builder(routeType).setSlotId(42).build();
assertAll(
() -> assertEquals(routeType, route.getRouteType()),
() -> assertEquals(42, route.getSlotId()),
() -> assertThrows(Throwable.class, () -> route.getSlotKey()));
}

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"PRIMARY_SLOT_KEY", "REPLICA_SLOT_KEY"})
public void build_with_slot_key(RouteType routeType) {
var route = new Route.Builder(routeType).setSlotKey("test").build();
assertAll(
() -> assertEquals(routeType, route.getRouteType()),
() -> assertEquals("test", route.getSlotKey()),
() -> assertThrows(Throwable.class, () -> route.getSlotId()));
}

@ParameterizedTest
@EnumSource(
value = RouteType.class,
names = {"ALL_NODES", "ALL_PRIMARIES", "RANDOM"})
public void build_simple_route(RouteType routeType) {
var route = new Route.Builder(routeType).build();
assertAll(
() -> assertEquals(routeType, route.getRouteType()),
() -> assertThrows(Throwable.class, () -> route.getSlotKey()),
() -> assertThrows(Throwable.class, () -> route.getSlotId()));
}
}

0 comments on commit 6d180ba

Please sign in to comment.