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 GET, SET, INFO and PING commands #894

Closed

Conversation

acarbonetto
Copy link
Contributor

@acarbonetto acarbonetto commented Feb 2, 2024

Issue #, if available:

Description of changes:
This PR adds get/set/ping/info calls to the standalone, and cluster-mode:

  • standalone
    • get
    • set
    • set with options
    • ping
    • ping with message
    • info
    • info with sections
  • cluster
    • get
    • set
    • set with options
    • ping
    • ping with message
    • info
    • info with routing
    • info with sections
    • info with sections and routing

Examples:

// Single commands
RedisClient client = RedisClient.createClient(config).get();
String pingResult = client.ping().get();
Ok setResult = client.set("key", "value").get();
String getResult = client.get("key").get(); // returns "value"
String infoResult = client.info().get();

// Cluster-mode single commands
RedisClusterClient clusterClient = RedisClusterClient.createClient(config).get();
ClusterValue<String> infoResult = clusterClient.info(ALL_NODES).get();
String infoForNode = infoResult.getMultiValue().get("<node address>");

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@acarbonetto acarbonetto requested a review from a team as a code owner February 2, 2024 04:57
@acarbonetto acarbonetto self-assigned this Feb 2, 2024
@acarbonetto acarbonetto added the java issues and fixes related to the java client label Feb 2, 2024
Copy link
Collaborator

@Yury-Fridlyand Yury-Fridlyand left a comment

Choose a reason for hiding this comment

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

Looking forward for IT

return Ok.INSTANCE;
}
String className = (value == null) ? "null" : value.getClass().getSimpleName();
throw new RedisException(
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this was non-null, should this log the actual value's toString() rather than just the class name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Better not to expose user data to our logging?

*/
public class Ok {

public static Ok INSTANCE = new Ok();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be static final, not just static.

@Yury-Fridlyand Yury-Fridlyand force-pushed the java/dev_acarbo_add_getsetpinginfo branch from 6785dc6 to 26ee2c3 Compare February 3, 2024 00:58
@Yury-Fridlyand Yury-Fridlyand changed the title Java: Add commands for GET/SET/INFO/PING Java: Add GET, SET, INFO and PING commands Feb 3, 2024
*
* @see <a href="https://redis.io/commands/?group=server">Server Management Commands</a>
*/
public interface ClusterServerCommands {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think ClusterManagementCommands describes the commands type better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ServerManagementClusterCommands or ServerClusterCommands.
We are going to have about 30 (15 for standalone and 15 for cluster mode) command interfaces to define all the types of commands. We can call them all ___Commands and ___ClusterCommands.
e.g. StringCommands and StringClusterCommands.

import java.util.concurrent.CompletableFuture;

/**
* Server Management Commands interface.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is it an interface? Why the commands aren't being implemented here? I can't think of two cluster clients that will implement it in a different way.
Also, The 'commands' folder name should be renamed to async commands, as these APIs won't serve sync clients

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • The big win is because we don't need to define javadocs in multiple files.
  • Another big win is that for non-routed calls that never take a route (like get, set) we can define the signature in an interface once, and all implementations afterwards must abide by the signature.
  • Finally, it also allows us to spread the javadocs over multiple files split by "type", which makes organizing and maintaining the calls easier.

*
* @see <a href="https://redis.io/commands/info/">redis.io</a> for details. {@link
* InfoOptions.Section#DEFAULT} option is assumed.
* @return Response from Redis cluster containing a <code>String</code>.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use the documentation from python/node. Since in Java we use overloading, you'll need to adjust the documentation for each command function.
From python:

        """Get information and statistics about the Redis server.
        See https://redis.io/commands/info/ for details.

        Args:
            sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of
            information to retrieve. When no parameter is provided, the default option is assumed.
            route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, in which
            case the client will route the command to the nodes defined by `route`. Defaults to None.

        Returns:
            TClusterResponse[str]: If a single node route is requested, returns a string containing the information for
            the required sections. Otherwise, returns a dict of strings, with each key containing the address of
            the queried node and value containing the information regarding the requested sections.
        """

so, for info() with no args in cluster mode usage, the command will always be routed to all primaries and the response will always be a dict of strings. Same for info(InfoOptions options). However, when route can be provided we need to use the ClusterValue type as the response is dependent on the user input (single node routing vs multi node). The API signature and documentation shall reflect all of that. Please rewrite the documentation and the signature of all functions

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 all commands in 8388c49

* 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>.
Copy link
Collaborator

Choose a reason for hiding this comment

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

same - please follow the functions' documentation we have in python/node throughout the PR

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 all commands in 8388c49

* @param key The key to store.
* @param value The value to store with the given key.
* @param options The Set options.
* @return Response from Redis containing a <code>String</code> or <code>null</code> response. The
Copy link
Collaborator

Choose a reason for hiding this comment

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

"Response from Redis containing a String or null response." I think it doesn't describe it well, it doesn't return a response that contains a string - it returns a string.

Again, follow the doc from python/node:

                If the value is successfully set, return OK.
                If value isn't set because of only_if_exists or only_if_does_not_exist conditions, return None.
                If return_old_value is set, return the old value as a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Response from Redis" is another way of saying that its wrapped by a CompletableFuture.
The rest of the comment applies and we will reorder to match the node/python as much as applicable.

@@ -19,7 +26,7 @@ private ClusterValue() {}

/**
* Get per-node value.<br>
* Check with {@link #hasMultiData()} prior to accessing the data.
* Asserts if {@link #hasMultiData()} is false.
Copy link
Collaborator

Choose a reason for hiding this comment

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

So it would raise an assertion error? I would expect it to throw some client error, the user shouldn't know he needs to also expect assertion errors in addition to the client errors

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It raises a RuntimeException. The user should always check hasMultiData() first to avoid the error.


/** Configuration of value lifetime. */
@Builder
public static final class TimeToLive {
Copy link
Collaborator

@barshaul barshaul Feb 4, 2024

Choose a reason for hiding this comment

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

Please keep the names we use in python/node (Expiry) - we want to maintain uniform naming conventions across the languages

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.

Changed field name to expiry of type Expiry:

private final Expiry expiry;
    public static final class Expiry {

        /** Expiry type for the time to live */
        private final ExpiryType type;

        /**
         * The amount of time to live before the key expires. Ignored when {@link
         * ExpiryType#KEEP_EXISTING} type is set.
         */
        private Long count;

* The amount of time to live before the key expires. Ignored when {@link
* TimeToLiveType#KEEP_EXISTING} type is set.
*/
private Integer count;
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: count => value

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it should be Long, as users will probably use Duration objects that will be converted to long values (e.g. https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html toSeconds, toMillis)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to keep count - consistent with Node. I think count is more meaningful, rather than value which is quite ambiguous.

/** Types of value expiration configuration. */
@RequiredArgsConstructor
@Getter
public enum TimeToLiveType {
Copy link
Collaborator

Choose a reason for hiding this comment

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

TimeToLiveType => ExpiryType

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 to ExpiryType

* 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.
* Represents a Response object from a Redis server with cluster-mode enabled. The response type may
Copy link
Contributor

Choose a reason for hiding this comment

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

this isn't a response, and certainly not a capital-R Response - this is the returned value. A response might be an error, which AFAIS isn't represented here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted

* 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.
* Represents a Response object from a Redis server with cluster-mode enabled. The response type may
Copy link
Contributor

Choose a reason for hiding this comment

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

how are the changes here related to this PR? can they be moved to another PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They could be split into a separate PR, actually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted. Will raise a new PR with these changes.

}

/** Configuration of value lifetime. */
@Builder
Copy link
Contributor

Choose a reason for hiding this comment

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

the builder pattern doesn't suit this - only 2 values, and both must be set.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This enforces a user to set count (aka value) even when expiration type is keepttl. Could be confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

not if you're using static factories -

static TimeToLive keep_existing()
static TimeToLive seconds(Duration seconds)
static TimeToLive milliseconds(Duration milliseconds)

etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in d082f32

Builder pattern for SetOptions now looks like:

SetOptions options = SetOptions.builder().expiry(UnixSeconds(100500L)).build();
SetOptions options = SetOptions.builder().expiry(Milliseconds(2000L)).build();
SetOptions options = SetOptions.builder().returnOldValue(true).conditionalSet(ONLY_IF_DOES_NOT_EXIST).expiry(KeepExisting()).build();

public <T> CompletableFuture<T> submitNewCommand(
RequestType requestType,
String[] arguments,
Optional<Route> route,
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: it's weird that route is optional in 2 senses - it's both an optional parameter due to functional overloading, and also a value of type Optional. Why would this function be called with Optional.empty(), instead of just calling the other overload?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reasoning is that one call is used for the standalone client, while the other method is used for the cluster client.
In the end, we merge into a single call with an Optional route to save on duplicating lines of code, but you are right, we could have implemented it the other way around.

@Test
public void ping_returns_success() {
// setup
CompletableFuture<String> testResponse = mock(CompletableFuture.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. you need to test all the commands with a real server
  2. given that you'll test all of the commands with a real server, what's the benefit of adding mock tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Main advantage is that it's faster/easier/cheaper to run unit tests and provides a good white-box testing platform. Using mocks allows us to test all variants independently (one injected dependency at a time).

If you don't want to maintain this long term, we could reduce/remove the unit tests, but I don't recommend that.

assertAll(
() -> assertTrue(value.hasSingleData()), () -> assertEquals(data, value.getSingleValue()));
}

@Test
@SneakyThrows
public void custom_command_with_multi_node_route_returns_multi_value() {
public void customCommand_with_multi_node_route_returns_multi_value() {
Copy link
Contributor

Choose a reason for hiding this comment

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

do these test renames belong in this PR? if not, please move them to a separate PR.

var commandManager = new TestCommandManager(null);

var data = Map.of("key1", "value1", "key2", "value2");
var client = new TestClient(commandManager, data);

var value = client.customCommand(new String[0], SimpleRoute.ALL_NODES).get();
var value = client.customCommand(ALL_NODES, "Test").get();
Copy link
Contributor

Choose a reason for hiding this comment

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

do these call changes belong in this PR? If not, please move them to another PR.


@SneakyThrows
@Test
public void customCommand_success() {
Copy link
Contributor

Choose a reason for hiding this comment

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

what does this test? how is it different from customCommand_returns_success?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removing this. We don't need both.

if (expiry.type != TimeToLiveType.KEEP_EXISTING) {
if (expiry.count == null) {
throw new RequestException(
"Set command received expiry type " + expiry.type + ", but count was not set.");
Copy link
Collaborator

@barshaul barshaul Feb 4, 2024

Choose a reason for hiding this comment

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

but count was not set => but value was not set

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to keep count, which is consistent with Node client.

@@ -84,7 +83,7 @@ public void channel_is_closed_when_disconnected_on_command() {
var channelHandler = new TestChannelHandler(callbackDispatcher);
var commandManager = new CommandManager(channelHandler);

var future = commandManager.submitNewCommand(createDummyCommand(), r -> null);
var future = commandManager.submitNewCommand(CustomCommand, new String[0], r -> null);
callbackDispatcher.completeRequest(null);
var exception = assertThrows(ExecutionException.class, future::get);
// a ClosingException thrown from CallbackDispatcher::completeRequest and then
Copy link
Collaborator

Choose a reason for hiding this comment

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

custom command with empty args list shouldn't close the client (e.g. raise ClosingException)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ClosingException is just from a mock. It's not necessarily from the empty args list.
A closing exception may be returned from any command call, and then we close the client.

acarbonetto and others added 12 commits February 6, 2024 08:56
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
@acarbonetto acarbonetto force-pushed the java/dev_acarbo_add_getsetpinginfo branch from ca28051 to 42b0498 Compare February 6, 2024 17:01
Copy link
Collaborator

@Yury-Fridlyand Yury-Fridlyand left a comment

Choose a reason for hiding this comment

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

Couple minor non-functional things

if (expiry != null) {
optionArgs.add(expiry.type.redisApi);
if (expiry.type != KEEP_EXISTING) {
if (expiry.count == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can omit this check with new constructors

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking the same thing, but does it hurt to keep it..?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe turn into an assert?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is ok to keep it

Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
@acarbonetto
Copy link
Contributor Author

@acarbonetto acarbonetto closed this Feb 8, 2024
@Yury-Fridlyand Yury-Fridlyand deleted the java/dev_acarbo_add_getsetpinginfo branch February 8, 2024 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
java issues and fixes related to the java client
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants