Skip to content

Commit 9345634

Browse files
authored
Java - Add custom command interface; and BaseCommands (valkey-io#837)
* Java - Add custom command interface; and BaseCommands (#58) * Add base command; add custom command --------- Signed-off-by: Andrew Carbonetto <[email protected]> * Clean up merge conflict Signed-off-by: Andrew Carbonetto <[email protected]> * Move Command resolvers to manager level Signed-off-by: Andrew Carbonetto <[email protected]> * Remove ClusterClient.java Signed-off-by: Andrew Carbonetto <[email protected]> * Spotless Signed-off-by: Andrew Carbonetto <[email protected]> * Update CommandManager comment Signed-off-by: Andrew Carbonetto <[email protected]> * Minor comments Signed-off-by: Andrew Carbonetto <[email protected]> * Move commands and response handlers to protected locations Signed-off-by: Andrew Carbonetto <[email protected]> * Clean up imports Signed-off-by: Andrew Carbonetto <[email protected]> * Update custom command documentation Signed-off-by: Andrew Carbonetto <[email protected]> * Update javadoc for RedisExceptionCheckedFunction Signed-off-by: Andrew Carbonetto <[email protected]> --------- Signed-off-by: Andrew Carbonetto <[email protected]>
1 parent 2bb441d commit 9345634

File tree

10 files changed

+487
-131
lines changed

10 files changed

+487
-131
lines changed

java/client/src/main/java/glide/api/BaseClient.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package glide.api;
22

3+
import glide.ffi.resolvers.RedisValueResolver;
4+
import glide.managers.BaseCommandResponseResolver;
35
import glide.managers.CommandManager;
46
import glide.managers.ConnectionManager;
57
import java.util.concurrent.ExecutionException;
68
import lombok.AllArgsConstructor;
9+
import response.ResponseOuterClass.Response;
710

811
/** Base Client class for Redis */
912
@AllArgsConstructor
@@ -12,6 +15,19 @@ public abstract class BaseClient implements AutoCloseable {
1215
protected ConnectionManager connectionManager;
1316
protected CommandManager commandManager;
1417

18+
/**
19+
* Extracts the response from the Protobuf response and either throws an exception or returns the
20+
* appropriate response as an Object
21+
*
22+
* @param response Redis protobuf message
23+
* @return Response Object
24+
*/
25+
protected static Object handleObjectResponse(Response response) {
26+
// return function to convert protobuf.Response into the response object by
27+
// calling valueFromPointer
28+
return (new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer)).apply(response);
29+
}
30+
1531
/**
1632
* Closes this resource, relinquishing any underlying resources. This method is invoked
1733
* automatically on objects managed by the try-with-resources statement.

java/client/src/main/java/glide/api/RedisClient.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import static glide.ffi.resolvers.SocketListenerResolver.getSocket;
44

5+
import glide.api.commands.BaseCommands;
56
import glide.api.models.configuration.RedisClientConfiguration;
67
import glide.connectors.handlers.CallbackDispatcher;
78
import glide.connectors.handlers.ChannelHandler;
89
import glide.managers.CommandManager;
910
import glide.managers.ConnectionManager;
11+
import glide.managers.models.Command;
1012
import java.util.concurrent.CompletableFuture;
1113

1214
/**
1315
* Async (non-blocking) client for Redis in Standalone mode. Use {@link
1416
* #CreateClient(RedisClientConfiguration)} to request a client to Redis.
1517
*/
16-
public class RedisClient extends BaseClient {
18+
public class RedisClient extends BaseClient implements BaseCommands {
1719

1820
/**
1921
* Request an async (non-blocking) Redis client in Standalone mode.
@@ -54,4 +56,26 @@ protected static CommandManager buildCommandManager(ChannelHandler channelHandle
5456
protected RedisClient(ConnectionManager connectionManager, CommandManager commandManager) {
5557
super(connectionManager, commandManager);
5658
}
59+
60+
/**
61+
* Executes a single command, without checking inputs. Every part of the command, including
62+
* subcommands, should be added as a separate value in args.
63+
*
64+
* @remarks This function should only be used for single-response commands. Commands that don't
65+
* return response (such as SUBSCRIBE), or that return potentially more than a single response
66+
* (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on
67+
* RESP2 connections) shouldn't be called using this function.
68+
* @example Returns a list of all pub/sub clients:
69+
* <pre>
70+
* Object result = client.customCommand(new String[]{"CLIENT","LIST","TYPE", "PUBSUB"}).get();
71+
* </pre>
72+
*
73+
* @param args arguments for the custom command
74+
* @return a CompletableFuture with response result from Redis
75+
*/
76+
public CompletableFuture<Object> customCommand(String[] args) {
77+
Command command =
78+
Command.builder().requestType(Command.RequestType.CUSTOM_COMMAND).arguments(args).build();
79+
return commandManager.submitNewCommand(command, BaseClient::handleObjectResponse);
80+
}
5781
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package glide.api.commands;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
5+
/** Base Commands interface to handle generic command and transaction requests. */
6+
public interface BaseCommands {
7+
8+
/**
9+
* Executes a single command, without checking inputs. Every part of the command, including
10+
* subcommands, should be added as a separate value in args.
11+
*
12+
* @remarks This function should only be used for single-response commands. Commands that don't
13+
* return response (such as SUBSCRIBE), or that return potentially more than a single response
14+
* (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on
15+
* RESP2 connections) shouldn't be called using this function.
16+
* @example Returns a list of all pub/sub clients:
17+
* <pre>
18+
* Object result = client.customCommand(new String[]{"CLIENT","LIST","TYPE", "PUBSUB"}).get();
19+
* </pre>
20+
*
21+
* @param args arguments for the custom command
22+
* @return a CompletableFuture with response result from Redis
23+
*/
24+
CompletableFuture<Object> customCommand(String[] args);
25+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package glide.managers;
2+
3+
import glide.api.models.exceptions.ClosingException;
4+
import glide.api.models.exceptions.ConnectionException;
5+
import glide.api.models.exceptions.ExecAbortException;
6+
import glide.api.models.exceptions.RedisException;
7+
import glide.api.models.exceptions.RequestException;
8+
import glide.api.models.exceptions.TimeoutException;
9+
import lombok.AllArgsConstructor;
10+
import response.ResponseOuterClass.RequestError;
11+
import response.ResponseOuterClass.Response;
12+
13+
/**
14+
* Response resolver responsible for evaluating the Redis response object with a success or failure.
15+
*/
16+
@AllArgsConstructor
17+
public class BaseCommandResponseResolver
18+
implements RedisExceptionCheckedFunction<Response, Object> {
19+
20+
private RedisExceptionCheckedFunction<Long, Object> respPointerResolver;
21+
22+
/**
23+
* Extracts value from the RESP pointer. <br>
24+
* Throws errors when the response is unsuccessful.
25+
*
26+
* @return A generic Object with the Response | null if the response is empty
27+
*/
28+
public Object apply(Response response) throws RedisException {
29+
if (response.hasRequestError()) {
30+
RequestError error = response.getRequestError();
31+
String msg = error.getMessage();
32+
switch (error.getType()) {
33+
case Unspecified:
34+
// Unspecified error on Redis service-side
35+
throw new RequestException(msg);
36+
case ExecAbort:
37+
// Transactional error on Redis service-side
38+
throw new ExecAbortException(msg);
39+
case Timeout:
40+
// Timeout from Glide to Redis service
41+
throw new TimeoutException(msg);
42+
case Disconnect:
43+
// Connection problem between Glide and Redis
44+
throw new ConnectionException(msg);
45+
default:
46+
// Request or command error from Redis
47+
throw new RequestException(msg);
48+
}
49+
}
50+
if (response.hasClosingError()) {
51+
// A closing error is thrown when Rust-core is not connected to Redis
52+
// We want to close shop and throw a ClosingException
53+
// TODO: close the channel on a closing error
54+
// channel.close();
55+
throw new ClosingException(response.getClosingError());
56+
}
57+
if (response.hasConstantResponse()) {
58+
// Return "OK"
59+
return response.getConstantResponse().toString();
60+
}
61+
if (response.hasRespPointer()) {
62+
// Return the shared value - which may be a null value
63+
return respPointerResolver.apply(response.getRespPointer());
64+
}
65+
// if no response payload is provided, assume null
66+
return null;
67+
}
68+
}
Lines changed: 42 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
package glide.managers;
22

3-
import glide.api.models.exceptions.ClosingException;
4-
import glide.api.models.exceptions.ConnectionException;
5-
import glide.api.models.exceptions.ExecAbortException;
6-
import glide.api.models.exceptions.RedisException;
7-
import glide.api.models.exceptions.RequestException;
8-
import glide.api.models.exceptions.TimeoutException;
93
import glide.connectors.handlers.ChannelHandler;
10-
import glide.ffi.resolvers.RedisValueResolver;
11-
import glide.models.RequestBuilder;
12-
import java.util.List;
4+
import glide.managers.models.Command;
135
import java.util.concurrent.CompletableFuture;
146
import lombok.RequiredArgsConstructor;
7+
import redis_request.RedisRequestOuterClass;
158
import redis_request.RedisRequestOuterClass.RequestType;
16-
import response.ResponseOuterClass.RequestError;
179
import response.ResponseOuterClass.Response;
1810

1911
/**
@@ -27,82 +19,56 @@ public class CommandManager {
2719
private final ChannelHandler channel;
2820

2921
/**
30-
* Async (non-blocking) get.<br>
31-
* See <a href="https://redis.io/commands/get/">REDIS docs for GET</a>.
22+
* Build a command and send.
3223
*
33-
* @param key The key name
24+
* @param command
25+
* @param responseHandler - to handle the response object
26+
* @return A result promise of type T
3427
*/
35-
public CompletableFuture<String> get(String key) {
36-
return submitNewRequest(RequestType.GetString, List.of(key));
28+
public <T> CompletableFuture<T> submitNewCommand(
29+
Command command, RedisExceptionCheckedFunction<Response, T> responseHandler) {
30+
// write command request to channel
31+
// when complete, convert the response to our expected type T using the given responseHandler
32+
return channel
33+
.write(prepareRedisRequest(command.getRequestType(), command.getArguments()), true)
34+
.thenApplyAsync(response -> responseHandler.apply(response));
3735
}
3836

3937
/**
40-
* Async (non-blocking) set.<br>
41-
* See <a href="https://redis.io/commands/set/">REDIS docs for SET</a>.
38+
* Build a protobuf command/transaction request object.<br>
39+
* Used by {@link CommandManager}.
4240
*
43-
* @param key The key name
44-
* @param value The value to set
41+
* @param command - Redis command
42+
* @param args - Redis command arguments as string array
43+
* @return An uncompleted request. CallbackDispatcher is responsible to complete it by adding a
44+
* callback id.
4545
*/
46-
public CompletableFuture<String> set(String key, String value) {
47-
return submitNewRequest(RequestType.SetString, List.of(key, value));
48-
}
46+
private RedisRequestOuterClass.RedisRequest.Builder prepareRedisRequest(
47+
Command.RequestType command, String[] args) {
48+
RedisRequestOuterClass.Command.ArgsArray.Builder commandArgs =
49+
RedisRequestOuterClass.Command.ArgsArray.newBuilder();
50+
for (var arg : args) {
51+
commandArgs.addArgs(arg);
52+
}
4953

50-
/**
51-
* Build a command and submit it Netty to send.
52-
*
53-
* @param command Command type
54-
* @param args Command arguments
55-
* @return A result promise
56-
*/
57-
private CompletableFuture<String> submitNewRequest(RequestType command, List<String> args) {
58-
return channel
59-
.write(RequestBuilder.prepareRedisRequest(command, args), true)
60-
.thenApplyAsync(this::extractValueFromGlideRsResponse);
54+
// TODO: set route properly when no RouteOptions given
55+
return RedisRequestOuterClass.RedisRequest.newBuilder()
56+
.setSingleCommand(
57+
RedisRequestOuterClass.Command.newBuilder()
58+
.setRequestType(mapRequestTypes(command))
59+
.setArgsArray(commandArgs.build())
60+
.build())
61+
.setRoute(
62+
RedisRequestOuterClass.Routes.newBuilder()
63+
.setSimpleRoutes(RedisRequestOuterClass.SimpleRoutes.AllNodes)
64+
.build());
6165
}
6266

63-
/**
64-
* Check response and extract data from it.
65-
*
66-
* @param response A response received from rust core lib
67-
* @return A String from the Redis response, or Ok. Otherwise, returns null
68-
*/
69-
private String extractValueFromGlideRsResponse(Response response) {
70-
if (response.hasRequestError()) {
71-
RequestError error = response.getRequestError();
72-
String msg = error.getMessage();
73-
switch (error.getType()) {
74-
case Unspecified:
75-
// Unspecified error on Redis service-side
76-
throw new RequestException(msg);
77-
case ExecAbort:
78-
// Transactional error on Redis service-side
79-
throw new ExecAbortException(msg);
80-
case Timeout:
81-
// Timeout from Glide to Redis service
82-
throw new TimeoutException(msg);
83-
case Disconnect:
84-
// Connection problem between Glide and Redis
85-
throw new ConnectionException(msg);
86-
default:
87-
// Request or command error from Redis
88-
throw new RedisException(msg);
89-
}
90-
}
91-
if (response.hasClosingError()) {
92-
// A closing error is thrown when Rust-core is not connected to Redis
93-
// We want to close shop and throw a ClosingException
94-
channel.close();
95-
throw new ClosingException(response.getClosingError());
96-
}
97-
if (response.hasConstantResponse()) {
98-
// Return "OK"
99-
return response.getConstantResponse().toString();
100-
}
101-
if (response.hasRespPointer()) {
102-
// Return the shared value - which may be a null value
103-
return RedisValueResolver.valueFromPointer(response.getRespPointer()).toString();
67+
private RequestType mapRequestTypes(Command.RequestType inType) {
68+
switch (inType) {
69+
case CUSTOM_COMMAND:
70+
return RequestType.CustomCommand;
10471
}
105-
// if no response payload is provided, assume null
106-
return null;
72+
throw new RuntimeException("Unsupported request type");
10773
}
10874
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package glide.managers;
2+
3+
import glide.api.models.exceptions.RedisException;
4+
5+
/**
6+
* Functional Interface to convert values and throw RedisException when encountering an error state.
7+
*
8+
* @param <R> type to evaluate
9+
* @param <T> payload type
10+
*/
11+
@FunctionalInterface
12+
public interface RedisExceptionCheckedFunction<R, T> {
13+
14+
/**
15+
* Functional response handler that takes a value of type R and returns a payload of type T.
16+
* Throws RedisException when encountering an invalid or error state.
17+
*
18+
* @param value - received value type
19+
* @return T - returning payload type
20+
* @throws RedisException
21+
*/
22+
T apply(R value) throws RedisException;
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package glide.managers.models;
2+
3+
import lombok.Builder;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Getter;
6+
import lombok.NonNull;
7+
8+
/** Base Command class to send a single request to Redis. */
9+
@Builder
10+
@Getter
11+
@EqualsAndHashCode
12+
public class Command {
13+
14+
/** Redis command request type */
15+
@NonNull final RequestType requestType;
16+
17+
/** List of Arguments for the Redis command request */
18+
@Builder.Default final String[] arguments = new String[] {};
19+
20+
public enum RequestType {
21+
/** Call a custom command with list of string arguments */
22+
CUSTOM_COMMAND,
23+
}
24+
}

0 commit comments

Comments
 (0)