Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Add PING command #914

Merged
merged 14 commits into from
Feb 9, 2024
52 changes: 45 additions & 7 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
package glide.api;

import static glide.ffi.resolvers.SocketListenerResolver.getSocket;
import static redis_request.RedisRequestOuterClass.RequestType.Ping;

import glide.api.commands.ConnectionManagementCommands;
import glide.api.models.configuration.BaseClientConfiguration;
import glide.api.models.exceptions.RedisException;
import glide.connectors.handlers.CallbackDispatcher;
import glide.connectors.handlers.ChannelHandler;
import glide.connectors.resources.Platform;
Expand All @@ -17,12 +20,12 @@
import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import response.ResponseOuterClass.Response;

/** Base Client class for Redis */
@AllArgsConstructor
public abstract class BaseClient implements AutoCloseable {

public abstract class BaseClient implements AutoCloseable, ConnectionManagementCommands {
protected final ConnectionManager connectionManager;
protected final CommandManager commandManager;

Expand Down Expand Up @@ -90,14 +93,49 @@ protected static CommandManager buildCommandManager(ChannelHandler channelHandle
}

/**
* Extracts the response from the Protobuf response and either throws an exception or returns the
* appropriate response as an <code>Object</code>.
* Extracts the value from a Redis response message and either throws an exception or returns the
* value as an object of type {@link T}. If <code>isNullable</code>, than also returns <code>null
* </code>.
*
* @param response Redis protobuf message
* @return Response <code>Object</code>
* @param classType Parameter {@link T} class type
* @param isNullable Accepts null values in the protobuf message
* @return Response as an object of type {@link T} or <code>null</code>
* @param <T> return type
* @throws RedisException on a type mismatch
*/
private <T> T handleRedisResponse(Class classType, boolean isNullable, Response response) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private <T> T handleRedisResponse(Class classType, boolean isNullable, Response response) {
protected <T> T handleRedisResponse(Class<T> classType, boolean isNullable, Response response) throws RedisException {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @shachlanAmazon for the idea

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in 2cbf558

Object value =
new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response);
if (isNullable && (value == null)) {
return null;
}
if (classType.isInstance(value)) {
return (T) value;
}
String className = value == null ? "null" : value.getClass().getSimpleName();
throw new RedisException(
"Unexpected return type from Redis: got "
+ className
+ " expected "
+ classType.toGenericString());
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
}

protected Object handleObjectResponse(Response response) {
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
// convert protobuf response into Object
return new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response);
return handleRedisResponse(Object.class, false, response);
}

protected String handleStringResponse(Response response) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessarily for this PR - this has a very similar structure to the handleArrayResponse.
Consider making a generic function with some refinement of this imaginary code:

protected T handleResponse<T>(Response response) {
        Object value = handleObjectResponse(response);
        if (value instanceof T || value == null) {
            return (T) value;
        }
        throw new RedisException(
                "Unexpected return type from Redis: got "
                        + value.getClass().getSimpleName()
                        + " expected " + T.getType());
    }
    ```
of course, `T.getType()` isn't really possible and this doesn't handle primitives well, but I believe this is possible, and will remove a lot of glue code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea. I've updated to include the generic function - this will reduce code duplication.

return handleRedisResponse(String.class, false, response);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should make sections/regions - for handlers, for commands for all rest

Copy link
Contributor Author

@acarbonetto acarbonetto Feb 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - but let's leave this for a future update. This will be useful when we have more than 3 commands.

@Override
public CompletableFuture<String> ping() {
return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse);
}

@Override
public CompletableFuture<String> ping(@NonNull String str) {
return commandManager.submitNewCommand(Ping, new String[] {str}, this::handleStringResponse);
}
}
6 changes: 4 additions & 2 deletions java/client/src/main/java/glide/api/RedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import glide.managers.CommandManager;
import glide.managers.ConnectionManager;
import java.util.concurrent.CompletableFuture;
import lombok.NonNull;

/**
* Async (non-blocking) client for Redis in Standalone mode. Use {@link #CreateClient} to request a
Expand All @@ -25,12 +26,13 @@ protected RedisClient(ConnectionManager connectionManager, CommandManager comman
* @param config Redis client Configuration
* @return A Future to connect and return a RedisClient
*/
public static CompletableFuture<RedisClient> CreateClient(RedisClientConfiguration config) {
public static CompletableFuture<RedisClient> CreateClient(
@NonNull RedisClientConfiguration config) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding for consistency across all methods. @nonnull will return a NullPointerException if the argument is not defined.

return CreateClient(config, RedisClient::new);
}

@Override
public CompletableFuture<Object> customCommand(String[] args) {
public CompletableFuture<Object> customCommand(@NonNull String[] args) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct me if I'm wrong, but @NonNull applies here to the array - the strings themselves might still be null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct... and that would cause a protobuf builder error (NullPointerException).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so how would you solve this?

return commandManager.submitNewCommand(CustomCommand, args, this::handleObjectResponse);
}
}
24 changes: 20 additions & 4 deletions java/client/src/main/java/glide/api/RedisClusterClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package glide.api;

import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand;
import static redis_request.RedisRequestOuterClass.RequestType.Ping;

import glide.api.commands.ConnectionManagementClusterCommands;
import glide.api.commands.GenericClusterCommands;
import glide.api.models.ClusterValue;
import glide.api.models.configuration.RedisClusterClientConfiguration;
Expand All @@ -11,12 +13,14 @@
import glide.managers.ConnectionManager;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import lombok.NonNull;

/**
* Async (non-blocking) client for Redis in Cluster mode. Use {@link #CreateClient} to request a
* client to Redis.
*/
public class RedisClusterClient extends BaseClient implements GenericClusterCommands {
public class RedisClusterClient extends BaseClient
implements ConnectionManagementClusterCommands, GenericClusterCommands {

protected RedisClusterClient(ConnectionManager connectionManager, CommandManager commandManager) {
super(connectionManager, commandManager);
Expand All @@ -29,20 +33,21 @@ protected RedisClusterClient(ConnectionManager connectionManager, CommandManager
* @return A Future to connect and return a RedisClusterClient
*/
public static CompletableFuture<RedisClusterClient> CreateClient(
RedisClusterClientConfiguration config) {
@NonNull RedisClusterClientConfiguration config) {
return CreateClient(config, RedisClusterClient::new);
}

@Override
public CompletableFuture<ClusterValue<Object>> customCommand(String[] args) {
public CompletableFuture<ClusterValue<Object>> customCommand(@NonNull String[] args) {
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
// TODO if a command returns a map as a single value, ClusterValue misleads user
return commandManager.submitNewCommand(
CustomCommand, args, response -> ClusterValue.of(handleObjectResponse(response)));
}

@Override
@SuppressWarnings("unchecked")
public CompletableFuture<ClusterValue<Object>> customCommand(String[] args, Route route) {
public CompletableFuture<ClusterValue<Object>> customCommand(
@NonNull String[] args, @NonNull Route route) {
return commandManager.submitNewCommand(
CustomCommand,
args,
Expand All @@ -52,4 +57,15 @@ public CompletableFuture<ClusterValue<Object>> customCommand(String[] args, Rout
? ClusterValue.ofSingleValue(handleObjectResponse(response))
: ClusterValue.ofMultiValue((Map<String, Object>) handleObjectResponse(response)));
}

@Override
public CompletableFuture<String> ping(@NonNull Route route) {
return commandManager.submitNewCommand(Ping, new String[0], route, this::handleStringResponse);
}

@Override
public CompletableFuture<String> ping(@NonNull String str, @NonNull Route route) {
return commandManager.submitNewCommand(
Ping, new String[] {str}, route, this::handleStringResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.commands;

import glide.api.models.configuration.RequestRoutingConfiguration.Route;
import java.util.concurrent.CompletableFuture;

/**
* Connection Management Commands interface.
*
* @see: <a href="https://redis.io/commands/?group=connection">Connection Management Commands</a>
*/
public interface ConnectionManagementClusterCommands {

/**
* Ping the Redis server.
*
* @see <a href="https://redis.io/commands/ping/">redis.io</a> for details.
* @param route Routing configuration for the command. Client will route the command to the nodes
* defined.
* @return Response from Redis containing a <code>String</code> with "PONG".
*/
CompletableFuture<String> ping(Route route);
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved

/**
* Ping the Redis server.
*
* @see <a href="https://redis.io/commands/ping/">redis.io</a> for details.
* @param str The ping argument that will be returned.
* @param route Routing configuration for the command. Client will route the command to the nodes
* defined.
* @return Response from Redis containing a <code>String</code> with a copy of the argument <code>
* str</code>.
*/
CompletableFuture<String> ping(String str, Route route);
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.commands;

import java.util.concurrent.CompletableFuture;

/**
* Connection Management Commands interface.
*
* @see: <a href="https://redis.io/commands/?group=connection">Connection Management Commands</a>
*/
public interface ConnectionManagementCommands {

/**
* Ping the Redis server.
*
* @see <a href="https://redis.io/commands/ping/">redis.io</a> for details.
* @return Response from Redis containing a <code>String</code> with "PONG".
*/
CompletableFuture<String> ping();

/**
* Ping the Redis server.
*
* @see <a href="https://redis.io/commands/ping/">redis.io</a> for details.
* @param str The ping argument that will be returned.
* @return Response from Redis containing a <code>String</code> with a copy of the argument <code>
* str</code>.
*/
CompletableFuture<String> ping(String str);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class CommandManager {
* @return A result promise of type T
*/
public <T> CompletableFuture<T> submitNewCommand(
RedisRequestOuterClass.RequestType requestType,
RequestType requestType,
String[] arguments,
RedisExceptionCheckedFunction<Response, T> responseHandler) {

Expand Down
43 changes: 43 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand;
import static redis_request.RedisRequestOuterClass.RequestType.Ping;

import glide.managers.CommandManager;
import glide.managers.ConnectionManager;
Expand Down Expand Up @@ -53,4 +54,46 @@ public void customCommand_returns_success() {
assertEquals(testResponse, response);
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void ping_returns_success() {
// setup
CompletableFuture<String> testResponse = mock(CompletableFuture.class);
when(testResponse.get()).thenReturn("PONG");

// match on protobuf request
when(commandManager.<String>submitNewCommand(eq(Ping), eq(new String[0]), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<String> response = service.ping();
String payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals("PONG", payload);
}

@SneakyThrows
@Test
public void ping_with_message_returns_success() {
// setup
String message = "RETURN OF THE PONG";
String[] arguments = new String[] {message};
CompletableFuture<String> testResponse = new CompletableFuture();
testResponse.complete(message);

// match on protobuf request
when(commandManager.<String>submitNewCommand(eq(Ping), eq(arguments), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<String> response = service.ping(message);
String pong = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(message, pong);
}
}
Loading
Loading