-
Notifications
You must be signed in to change notification settings - Fork 70
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
Java: Add GET
, SET
, INFO
and PING
commands
#894
Conversation
There was a problem hiding this 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
java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java
Outdated
Show resolved
Hide resolved
java/client/src/test/java/glide/api/RedisClusterClientTest.java
Outdated
Show resolved
Hide resolved
java/client/src/test/java/glide/managers/CommandManagerTest.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/commands/StringCommands.java
Outdated
Show resolved
Hide resolved
return Ok.INSTANCE; | ||
} | ||
String className = (value == null) ? "null" : value.getClass().getSimpleName(); | ||
throw new RedisException( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
java/client/src/main/java/glide/api/models/commands/SetOptions.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/models/commands/SetOptions.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/models/commands/SetOptions.java
Outdated
Show resolved
Hide resolved
6785dc6
to
26ee2c3
Compare
GET
, SET
, INFO
and PING
commands
* | ||
* @see <a href="https://redis.io/commands/?group=server">Server Management Commands</a> | ||
*/ | ||
public interface ClusterServerCommands { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
java/client/src/main/java/glide/api/commands/StringCommands.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/commands/StringCommands.java
Outdated
Show resolved
Hide resolved
* @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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: count => value
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TimeToLiveType => ExpiryType
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted
java/client/src/main/java/glide/api/commands/ClusterBaseCommands.java
Outdated
Show resolved
Hide resolved
* 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- you need to test all the commands with a real server
- given that you'll test all of the commands with a real server, what's the benefit of adding mock tests?
There was a problem hiding this comment.
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.
java/client/src/test/java/glide/api/RedisClusterClientTest.java
Outdated
Show resolved
Hide resolved
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() { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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."); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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: 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]>
ca28051
to
42b0498
Compare
java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java
Outdated
Show resolved
Hide resolved
java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java
Show resolved
Hide resolved
java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java
Show resolved
Hide resolved
java/client/src/main/java/glide/api/commands/GenericCommands.java
Outdated
Show resolved
Hide resolved
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]>
There was a problem hiding this 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
java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java
Show resolved
Hide resolved
if (expiry != null) { | ||
optionArgs.add(expiry.type.redisApi); | ||
if (expiry.type != KEEP_EXISTING) { | ||
if (expiry.count == null) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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..?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
java/client/src/test/java/glide/api/RedisClusterClientTest.java
Outdated
Show resolved
Hide resolved
java/client/src/test/java/glide/api/RedisClusterClientTest.java
Outdated
Show resolved
Hide resolved
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]>
Signed-off-by: Andrew Carbonetto <[email protected]>
java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java
Show resolved
Hide resolved
Closing PR in favour of: |
Issue #, if available:
Description of changes:
This PR adds get/set/ping/info calls to the standalone, and cluster-mode:
Examples:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.