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

Add cluster client, request routes configuration and support for bulk response #59

Conversation

Yury-Fridlyand
Copy link

@Yury-Fridlyand Yury-Fridlyand commented Jan 13, 2024

Depends on:

Features:

  1. Cluster client
  2. Cliend API to execute commands
  3. Added option to execute every commands with routing settings
  4. Added data conversion step: basic client converts data to string, cluster client converts to RedisValue, which is union to a string and a map

Use case:

var client =
    ClusterClient.CreateClient(
            RedisClusterClientConfiguration.builder()
                .address(
                    NodeAddress.builder()
                        .host("cluster.redis.com")
                        .build())
                .useTLS(true)
                .build())
        .get();

var cmdRes1 = client.getCommandManager().get("956231").get();
// cmdRes1 contains a multi object => cmdRes1.hasMultiData() == true
//   <node1> : <value on node1>
//   <node2> : <value on node2>
//   <node3> : <value on node3>

// trying the same request to route to a single node:
var cmdRes2 = client.getCommandManager().get("956231", Route.builder().routeType(Route.RouteType.Random).build()).get();
// cmdRes2 contains a single object => cmdRes1.hasBulkData() == false
//   <value>

// another examples:
RedisValue res1 = clusterClient.customCommand(new String[] {"set", "kkk", "vvv"}).get();
RedisValue res2 = clusterClient.customCommand(
		new String[] {"get", "kkk"},
		Route.builder().routeType(Route.RouteType.AllNodes).build())
	.get();
RedisValue res3 = clusterClient.customCommand(
		new String[] {"get", "kkk"},
		Route.builder().routeType(Route.RouteType.Random).build())
	.get();
RedisValue res4 = clusterClient.customCommand(
		new String[] {"get", "kkk"},
		Route.builder().routeType(Route.RouteType.PrimarySlotId).slotId(42).build())
	.get();

Meanwhile a regular client has no option to set routes to a command.
image
image

java/client/src/main/java/glide/api/BaseClient.java Outdated Show resolved Hide resolved
* <li>{@link SlotIdRoute}
* </ul>
*/
public interface Routes {}

Choose a reason for hiding this comment

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

as much as I prefer this layout, we should instead follow the existing format of their configurations in Python/Node.

e.g.
https://github.com/Bit-Quill/glide-for-redis/blob/main/node/src/RedisClusterClient.ts#L51

Copy link
Author

Choose a reason for hiding this comment

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

I followed protobuf definitions.

Choose a reason for hiding this comment

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

This would be inconsistent with how we're going to implement the SetOptions

Copy link
Author

Choose a reason for hiding this comment

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

Reworked

java/src/lib.rs Outdated Show resolved Hide resolved
Copy link

@acarbonetto acarbonetto left a comment

Choose a reason for hiding this comment

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

You are going to have to refactor a lot of code if you don't rebase against #49.
For example, I'm not sure the changes in CommandManager and CusterCommandManager will be necessary, as the get/set calls were removed from these classes.

* <li>{@link SlotIdRoute}
* </ul>
*/
public interface Routes {}

Choose a reason for hiding this comment

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

This would be inconsistent with how we're going to implement the SetOptions

java/client/src/main/java/glide/api/RedisClient.java Outdated Show resolved Hide resolved
@Yury-Fridlyand Yury-Fridlyand force-pushed the java/dev_yuryf_cluster_bulk_resp branch from 8eaf785 to b7f9bbd Compare January 20, 2024 03:10
* @return a CompletableFuture with response result from Redis
*/
CompletableFuture<Object> customCommand(String[] args);
CompletableFuture<T> customCommand(String[] args);

Choose a reason for hiding this comment

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

The whole point of customCommand is that we won't know the return type. We should return Object in all cases.

Copy link
Author

@Yury-Fridlyand Yury-Fridlyand Jan 22, 2024

Choose a reason for hiding this comment

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

For customCommand:
T = Object for standalone client
T = RedisValue for cluster client

Choose a reason for hiding this comment

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

It would be better to duplicate. Why don't we just implement BaseCommands for the RedisClient, and ClusterBaseCommands for the RedisClusterClient? I don't see the value to using a generic here when there are only two possible options.

Copy link
Author

Choose a reason for hiding this comment

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

Actually, more than two, becase now T is RedisValue<V> for cluster client. So having T we avoid code duplicating N times for sake of maintanability and so on.

*
* @param <T> The data return type.
*/
public interface BaseCommands<T> {

Choose a reason for hiding this comment

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

BaseCommands don't have return types. The RedisValue return type would only be value for cluster mode, standalone mode should have a type t defined.
We should show how this works for both cluster mode and standalone mode. Maybe also prototype how this would work with a string handler, which returns a RedisValue...?

@Getter
public class RedisValue {
/** Get per-node value. */
private Map<String, Object> multiValue = null;

Choose a reason for hiding this comment

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

In some cases, we should know what the return type would be. For example, for CONFIG GET, we know we get a Map back from Redis for each node.
The single value would also be a Map. We should include a type in this object that defines the return type for single Value or the value type of the map for multiValue.

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Reworked, RedisValue has a type parameter as of now


public enum RouteType {
/** Route request to all nodes. */
AllNodes,

Choose a reason for hiding this comment

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

Feels like we should follow the convention to capitalize all enums. No?

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 6cbc27f

@AllArgsConstructor
public abstract class BaseClient implements AutoCloseable {
public abstract class BaseClient<T> implements AutoCloseable {

Choose a reason for hiding this comment

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

Why is the type T attached/defined in the client. Would the type be defined per-request command, and not for the entire client? I'd like to see how this works for both standalone and cluster commands when the type returned is known (not customCommand).

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 2b05632.


/**
* Extracts the response from the Protobuf response and either throws an exception or returns the
* appropriate response has an Object

Choose a reason for hiding this comment

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

small typo

Suggested change
* appropriate response has an Object
* appropriate response as an Object

Copy link
Author

Choose a reason for hiding this comment

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

Fixed thanks

*
* @param <T> The wrapped data type
*/
@Getter

Choose a reason for hiding this comment

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

should we use a straight getter here, or does it make better sense to throw an exception if the wrong call is made?

Copy link
Author

Choose a reason for hiding this comment

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

Updated

@@ -58,6 +58,7 @@ public ChannelHandler(
.channel(domainSocketChannelClass)
.handler(channelInitializer)
.connect(domainSocketAddress)
.syncUninterruptibly()

Choose a reason for hiding this comment

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

maybe for a separate PR?

Copy link
Author

Choose a reason for hiding this comment

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

Rebased on main, where sync is used

* @return A result promise of type T
*/
public <T> CompletableFuture<T> submitNewCommand(
Command command, RedisExceptionCheckedFunction<Response, T> responseHandler, Route route) {

Choose a reason for hiding this comment

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

I think it looks cleaner if the route is before the responseHandler?

Copy link
Author

Choose a reason for hiding this comment

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

Reordered

* @param route node routing configuration for the command
* @return CompletableFuture with the response
*/
CompletableFuture<T> customCommand(String[] args, Route route);

Choose a reason for hiding this comment

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

Why doesn't this return CompletableFuture<RedisValue<Object>>?

Copy link
Author

Choose a reason for hiding this comment

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

Because cluster client implements this interface with

    @Override
    public CompletableFuture<RedisValue<Object>> customCommand(String[] args)
    ...

* @return a CompletableFuture with response result from Redis
*/
CompletableFuture<Object> customCommand(String[] args);
CompletableFuture<T> customCommand(String[] args);

Choose a reason for hiding this comment

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

It would be better to duplicate. Why don't we just implement BaseCommands for the RedisClient, and ClusterBaseCommands for the RedisClusterClient? I don't see the value to using a generic here when there are only two possible options.

* @param <T> The wrapped data type
*/
@Getter
public class RedisValue<T> {

Choose a reason for hiding this comment

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

I'm not sure if we've discussed this already (I feel like we may have), but would ClusterValue be a better name? It seems we don't use this for standalone clients.

Copy link
Author

Choose a reason for hiding this comment

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

Renamed, thanks

Copy link

@jonathanl-bq jonathanl-bq left a comment

Choose a reason for hiding this comment

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

Approved, but we should make sure to test valid configurations once the integration tests PR is merged.

@Yury-Fridlyand Yury-Fridlyand force-pushed the java/dev_yuryf_cluster_bulk_resp branch from dab3bc3 to 2639c41 Compare January 25, 2024 21:01
@Yury-Fridlyand Yury-Fridlyand force-pushed the java/integ_yuryf_cluster_bulk_resp branch from b2e7b60 to 9345634 Compare January 25, 2024 23:29
Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
* Async (non-blocking) client for Redis in Cluster mode. Use {@link #CreateClient} to request a
* client to Redis.
*/
public class ClusterClient extends BaseClient implements ClusterBaseCommands<ClusterValue<Object>> {

Choose a reason for hiding this comment

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

Rename to RedisClusterClient.

Copy link
Author

Choose a reason for hiding this comment

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

Updated in 62cf838

*
* @param <T> The data return type.
*/
public interface ClusterBaseCommands<T> extends BaseCommands<T> {

Choose a reason for hiding this comment

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

I'd need to see how this will work for StringCommands, HashCommands, etc.
I think this model is overly complex just to save a single line in an interface. And if we have several types (StringCommands will need around 4 generic types defined) this starts to look really ugly.

Copy link
Author

Choose a reason for hiding this comment

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

Changed in 62cf838

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

@jonathanl-bq jonathanl-bq left a comment

Choose a reason for hiding this comment

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

Reviewed the changes since yesterday only, but seems fine to me still.

Copy link

@acarbonetto acarbonetto left a comment

Choose a reason for hiding this comment

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

Fairly minor comments. Thank you for updating the BaseCommands and BaseClusterCommands. I think this is the best solution moving forward.

// return function to convert protobuf.Response into the response object by
// calling valueFromPointer
return (new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer)).apply(response);
protected Object handleObjectResponse(Response response) {

Choose a reason for hiding this comment

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

Not opposed, but why not static?

Copy link
Author

Choose a reason for hiding this comment

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

no idea

* @param responseHandler The handler for the response object
* @return A result promise of type T
*/
public <T> CompletableFuture<T> submitNewCommand(

Choose a reason for hiding this comment

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

Is it better to using Optional(Route) and avoid overloading and duplicating code?

import java.util.Map;

/**
* A union-like type which can store single or bulk value retrieved from Redis.

Choose a reason for hiding this comment

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

Suggested change
* A union-like type which can store single or bulk value retrieved from Redis.
* A union-like type which can store single-value or multi-value retrieved from Redis. The multi-value, if defined, contains the routed value as a Map<String, Object> containing a cluster node address to cluster node value.


/** Get the single value. */
public T getSingleValue() {
assert !hasMultiData();

Choose a reason for hiding this comment

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

nit: is it worth creating a hasSingleData() method?


private ClusterValue() {}

/** Get per-node value. */

Choose a reason for hiding this comment

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

We will need a more explicit javadoc if we're going to expose this to the user.

Please mention that hasMultiValue() should be called first to avoid exceptions

}

/** Get the single value. */
public T getSingleValue() {

Choose a reason for hiding this comment

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

javadoc - please mention that hasSingleValue() should be called first to avoid exceptions

}

@SuppressWarnings("unchecked")
public static <T> ClusterValue<T> of(Object data) {

Choose a reason for hiding this comment

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

javadoc

}

/** Get the value type. Use it prior to accessing the data. */
public boolean hasMultiData() {

Choose a reason for hiding this comment

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

javadoc - please be more specific since this is exposed to the user.

Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
@Yury-Fridlyand Yury-Fridlyand merged commit f296c00 into java/integ_yuryf_cluster_bulk_resp Jan 27, 2024
10 checks passed
@Yury-Fridlyand Yury-Fridlyand deleted the java/dev_yuryf_cluster_bulk_resp branch January 27, 2024 00:16
Yury-Fridlyand added a commit that referenced this pull request Feb 1, 2024
… bulk response (valkey-io#864)

* Add cluster client, request routes configuration and support for bulk response (#59)


Signed-off-by: Yury-Fridlyand <[email protected]>
acarbonetto pushed a commit that referenced this pull request Feb 1, 2024
… 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants