From dc79586efc5933447a8cee751ab0f9ed43c5ab57 Mon Sep 17 00:00:00 2001 From: Niharika Bhavaraju <31915502+niharikabhavaraju@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:46:28 +0530 Subject: [PATCH 1/2] Go: Implement Custom Command with route(Generic Cluster cmd), Ping with Route (#2979) * Custom Cmd(Generic Cluster cmd), Ping with Route Signed-off-by: Niharika Bhavaraju Signed-off-by: Yury-Fridlyand Co-authored-by: Yury-Fridlyand --- go/api/base_client.go | 75 ------------- .../connection_management_cluster_commands.go | 16 +++ go/api/connection_management_commands.go | 4 +- go/api/generic_cluster_commands.go | 4 + go/api/glide_client.go | 71 ++++++++++++ go/api/glide_cluster_client.go | 106 ++++++++++++++++++ go/api/options/ping_options.go | 31 +++++ go/integTest/cluster_commands_test.go | 80 +++++++++++++ go/integTest/shared_commands_test.go | 28 ----- go/integTest/standalone_commands_test.go | 54 +++++++++ 10 files changed, 365 insertions(+), 104 deletions(-) create mode 100644 go/api/connection_management_cluster_commands.go create mode 100644 go/api/options/ping_options.go diff --git a/go/api/base_client.go b/go/api/base_client.go index a989573d93..dd8c419710 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -31,7 +31,6 @@ type BaseClient interface { SetCommands StreamCommands SortedSetCommands - ConnectionManagementCommands HyperLogLogCommands GenericBaseCommands BitmapCommands @@ -3104,52 +3103,6 @@ func (client *baseClient) BLMove( return handleStringOrNilResponse(result) } -// Pings the server. -// -// Return value: -// -// Returns "PONG". -// -// For example: -// -// result, err := client.Ping() -// -// [valkey.io]: https://valkey.io/commands/ping/ -func (client *baseClient) Ping() (string, error) { - result, err := client.executeCommand(C.Ping, []string{}) - if err != nil { - return defaultStringResponse, err - } - - return handleStringResponse(result) -} - -// Pings the server with a custom message. -// -// Parameters: -// -// message - A message to include in the `PING` command. -// -// Return value: -// -// Returns the copy of message. -// -// For example: -// -// result, err := client.PingWithMessage("Hello") -// -// [valkey.io]: https://valkey.io/commands/ping/ -func (client *baseClient) PingWithMessage(message string) (string, error) { - args := []string{message} - - result, err := client.executeCommand(C.Ping, args) - if err != nil { - return defaultStringResponse, err - } - - return handleStringResponse(result) -} - // Del removes the specified keys from the database. A key is ignored if it does not exist. // // Note: @@ -5595,34 +5548,6 @@ func (client *baseClient) ObjectEncoding(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -// Echo the provided message back. -// The command will be routed a random node. -// -// Parameters: -// -// message - The provided message. -// -// Return value: -// -// The provided message -// -// For example: -// -// result, err := client.Echo("Hello World") -// if err != nil { -// // handle error -// } -// fmt.Println(result.Value()) // Output: Hello World -// -// [valkey.io]: https://valkey.io/commands/echo/ -func (client *baseClient) Echo(message string) (Result[string], error) { - result, err := client.executeCommand(C.Echo, []string{message}) - if err != nil { - return CreateNilStringResult(), err - } - return handleStringOrNilResponse(result) -} - // Destroys the consumer group `group` for the stream stored at `key`. // // See [valkey.io] for details. diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go new file mode 100644 index 0000000000..61d524d1b9 --- /dev/null +++ b/go/api/connection_management_cluster_commands.go @@ -0,0 +1,16 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// Supports commands and transactions for the "Connection Management" group of commands for cluster client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#connection +type ConnectionManagementClusterCommands interface { + Ping() (string, error) + + PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) +} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index 480b85af91..ba9a77ff8b 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + // Supports commands and transactions for the "Connection Management" group of commands for standalone client. // // See [valkey.io] for details. @@ -10,7 +12,7 @@ package api type ConnectionManagementCommands interface { Ping() (string, error) - PingWithMessage(message string) (string, error) + PingWithOptions(pingOptions options.PingOptions) (string, error) Echo(message string) (Result[string], error) } diff --git a/go/api/generic_cluster_commands.go b/go/api/generic_cluster_commands.go index 3d5186caa3..44b656f885 100644 --- a/go/api/generic_cluster_commands.go +++ b/go/api/generic_cluster_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + // GenericClusterCommands supports commands for the "Generic Commands" group for cluster client. // // See [valkey.io] for details. @@ -9,4 +11,6 @@ package api // [valkey.io]: https://valkey.io/commands/#generic type GenericClusterCommands interface { CustomCommand(args []string) (ClusterValue[interface{}], error) + + CustomCommandWithRoute(args []string, route config.Route) (ClusterValue[interface{}], error) } diff --git a/go/api/glide_client.go b/go/api/glide_client.go index cefda9b8d2..6fa46fefa5 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -7,6 +7,7 @@ package api import "C" import ( + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/valkey-io/valkey-glide/go/glide/utils" ) @@ -19,6 +20,7 @@ type GlideClientCommands interface { GenericCommands ServerManagementCommands BitmapCommands + ConnectionManagementCommands } // GlideClient implements standalone mode operations by extending baseClient functionality. @@ -225,3 +227,72 @@ func (client *GlideClient) DBSize() (int64, error) { } return handleIntResponse(result) } + +// Echo the provided message back. +// The command will be routed a random node. +// +// Parameters: +// +// message - The provided message. +// +// Return value: +// +// The provided message +// +// For example: +// +// result, err := client.Echo("Hello World") +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: Hello World +// +// [valkey.io]: https://valkey.io/commands/echo/ +func (client *GlideClient) Echo(message string) (Result[string], error) { + result, err := client.executeCommand(C.Echo, []string{message}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Pings the server. +// +// Return value: +// +// Returns "PONG". +// +// For example: +// +// result, err := client.Ping() +// fmt.Println(result) // Output: PONG +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClient) Ping() (string, error) { + return client.PingWithOptions(options.PingOptions{}) +} + +// Pings the server. +// +// Parameters: +// +// pingOptions - The PingOptions type. +// +// Return value: +// +// Returns the copy of message. +// +// For example: +// +// options := options.NewPingOptionsBuilder().SetMessage("hello") +// result, err := client.PingWithOptions(options) +// result: "hello" +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClient) PingWithOptions(pingOptions options.PingOptions) (string, error) { + result, err := client.executeCommand(C.Ping, pingOptions.ToArgs()) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 5a907286fe..17ea2259e0 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -6,6 +6,11 @@ package api // #include "../lib.h" import "C" +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" + "github.com/valkey-io/valkey-glide/go/glide/api/options" +) + // GlideClusterClient interface compliance check. var _ GlideClusterClientCommands = (*GlideClusterClient)(nil) @@ -14,6 +19,7 @@ type GlideClusterClientCommands interface { BaseClient GenericClusterCommands ServerManagementClusterCommands + ConnectionManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. @@ -160,3 +166,103 @@ func (client *GlideClusterClient) InfoWithOptions(options ClusterInfoOptions) (C } return createClusterSingleValue[string](data), nil } + +// CustomCommandWithRoute executes a single command, specified by args, without checking inputs. Every part of the command, +// including the command name and subcommands, should be added as a separate value in args. The returning value depends on +// the executed command. +// +// See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. +// +// Parameters: +// +// args - Arguments for the custom command including the command name. +// route - Specifies the routing configuration for the command. The client will route the +// command to the nodes defined by route. +// +// Return value: +// +// The returning value depends on the executed command and route. +// +// For example: +// +// route := config.SimpleNodeRoute(config.RandomRoute) +// result, err := client.CustomCommandWithRoute([]string{"ping"}, route) +// result.SingleValue().(string): "PONG" +// +// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command +func (client *GlideClusterClient) CustomCommandWithRoute( + args []string, + route config.Route, +) (ClusterValue[interface{}], error) { + res, err := client.executeCommandWithRoute(C.CustomCommand, args, route) + if err != nil { + return createEmptyClusterValue[interface{}](), err + } + data, err := handleInterfaceResponse(res) + if err != nil { + return createEmptyClusterValue[interface{}](), err + } + return createClusterValue[interface{}](data), nil +} + +// Pings the server. +// The command will be routed to all primary nodes. +// +// Return value: +// +// Returns "PONG". +// +// For example: +// +// result, err := clusterClient.Ping() +// fmt.Println(result) // Output: PONG +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClusterClient) Ping() (string, error) { + result, err := client.executeCommand(C.Ping, []string{}) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + +// Pings the server. +// The command will be routed to all primary nodes, unless `Route` is provided in `pingOptions`. +// +// Parameters: +// +// pingOptions - The PingOptions type. +// +// Return value: +// +// Returns the copy of message. +// +// For example: +// +// route := config.Route(config.RandomRoute) +// opts := options.ClusterPingOptions{ +// PingOptions: &options.PingOptions{ +// Message: "Hello", +// }, +// Route: &route, +// } +// result, err := clusterClient.PingWithOptions(opts) +// fmt.Println(result) // Output: Hello +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClusterClient) PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) { + if pingOptions.Route == nil { + response, err := client.executeCommand(C.Ping, pingOptions.ToArgs()) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(response) + } + + response, err := client.executeCommandWithRoute(C.Ping, pingOptions.ToArgs(), *pingOptions.Route) + if err != nil { + return defaultStringResponse, err + } + + return handleStringResponse(response) +} diff --git a/go/api/options/ping_options.go b/go/api/options/ping_options.go new file mode 100644 index 0000000000..dc5c527ff9 --- /dev/null +++ b/go/api/options/ping_options.go @@ -0,0 +1,31 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" +) + +// Optional arguments for `Ping` for standalone client +type PingOptions struct { + Message string +} + +// Optional arguments for `Ping` for cluster client +type ClusterPingOptions struct { + *PingOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by *Route*. + Route *config.Route +} + +func (opts *PingOptions) ToArgs() []string { + if opts == nil { + return []string{} + } + args := []string{} + if opts.Message != "" { + args = append(args, opts.Message) + } + return args +} diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 50693b5b17..3c329e2bdc 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/valkey-io/valkey-glide/go/glide/api" "github.com/valkey-io/valkey-glide/go/glide/api/config" + "github.com/valkey-io/valkey-glide/go/glide/api/options" ) func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { @@ -106,3 +107,82 @@ func (suite *GlideTestSuite) TestInfoCluster() { } } } + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_Info() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.AllPrimaries) + result, err := client.CustomCommandWithRoute([]string{"INFO"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsMultiValue()) + multiValue := result.MultiValue() + for _, value := range multiValue { + assert.True(suite.T(), strings.Contains(value.(string), "# Stats")) + } +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_Echo() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.RandomRoute) + result, err := client.CustomCommandWithRoute([]string{"ECHO", "GO GLIDE GO"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsSingleValue()) + assert.Equal(suite.T(), "GO GLIDE GO", result.SingleValue().(string)) +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_InvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.NewByAddressRoute("invalidHost", 9999) + result, err := client.CustomCommandWithRoute([]string{"PING"}, invalidRoute) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), result.IsEmpty()) +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_AllNodes() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.AllNodes) + result, err := client.CustomCommandWithRoute([]string{"PING"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsSingleValue()) + assert.Equal(suite.T(), "PONG", result.SingleValue()) +} + +func (suite *GlideTestSuite) TestPingWithOptions_NoRoute() { + client := suite.defaultClusterClient() + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: nil, + } + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_WithRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.AllNodes) + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: &route, + } + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_InvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: &invalidRoute, + } + result, err := client.PingWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Empty(suite.T(), result) +} diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index b2fd7eacd4..e6cece6883 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -603,23 +603,6 @@ func (suite *GlideTestSuite) TestGetDel_EmptyKey() { }) } -func (suite *GlideTestSuite) TestPing_NoArgument() { - suite.runWithDefaultClients(func(client api.BaseClient) { - result, err := client.Ping() - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "PONG", result) - }) -} - -func (suite *GlideTestSuite) TestPing_WithArgument() { - suite.runWithDefaultClients(func(client api.BaseClient) { - // Passing "Hello" as the message - result, err := client.PingWithMessage("Hello") - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "Hello", result) - }) -} - func (suite *GlideTestSuite) TestHSet_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { fields := map[string]string{"field1": "value1", "field2": "value2"} @@ -6223,17 +6206,6 @@ func (suite *GlideTestSuite) TestRestoreWithOptions() { }) } -func (suite *GlideTestSuite) TestEcho() { - suite.runWithDefaultClients(func(client api.BaseClient) { - // Test 1: Check if Echo command return the message - value := "Hello world" - t := suite.T() - resultEcho, err := client.Echo(value) - assert.Nil(t, err) - assert.Equal(t, value, resultEcho.Value()) - }) -} - func (suite *GlideTestSuite) TestZRemRangeByRank() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := uuid.New().String() diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 63b07ac3b1..4bede26248 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -429,3 +429,57 @@ func (suite *GlideTestSuite) TestDBSize() { assert.Nil(suite.T(), err) assert.Greater(suite.T(), result, int64(0)) } + +func (suite *GlideTestSuite) TestPing_NoArgument() { + client := suite.defaultClient() + + result, err := client.Ping() + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "PONG", result) +} + +func (suite *GlideTestSuite) TestEcho() { + client := suite.defaultClient() + // Test 1: Check if Echo command return the message + value := "Hello world" + t := suite.T() + resultEcho, err := client.Echo(value) + assert.Nil(t, err) + assert.Equal(t, value, resultEcho.Value()) +} + +func (suite *GlideTestSuite) TestPing_ClosedClient() { + client := suite.defaultClient() + client.Close() + + result, err := client.Ping() + + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} + +func (suite *GlideTestSuite) TestPingWithOptions_WithMessage() { + client := suite.defaultClient() + options := options.PingOptions{ + Message: "hello", + } + + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_ClosedClient() { + client := suite.defaultClient() + client.Close() + + options := options.PingOptions{ + Message: "hello", + } + + result, err := client.PingWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} From 17a91b2f468b90b5d1512956c98c116c91b9c6c4 Mon Sep 17 00:00:00 2001 From: James Xin Date: Wed, 29 Jan 2025 12:41:57 -0800 Subject: [PATCH 2/2] Java: Fix lpopCount null handling (#3025) Java: Fix lpopCount null handling --------- Signed-off-by: James Xin --- CHANGELOG.md | 1 + java/client/src/main/java/glide/api/BaseClient.java | 4 ++-- java/integTest/src/test/java/glide/SharedCommandTests.java | 2 ++ .../src/test/java/glide/standalone/StandaloneClientTests.java | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 951db65fda..d794567e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * Node: Fix `zrangeWithScores` (disallow `RangeByLex` as it is not supported) ([#2926](https://github.com/valkey-io/valkey-glide/pull/2926)) * Core: improve fix in #2381 ([#2929](https://github.com/valkey-io/valkey-glide/pull/2929)) +* Java: Fix `lpopCount` null handling ([#3025](https://github.com/valkey-io/valkey-glide/pull/3025)) #### Operational Enhancements diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6039f84e8a..45e16d0107 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1384,7 +1384,7 @@ public CompletableFuture lpopCount(@NonNull String key, long count) { return commandManager.submitNewCommand( LPop, new String[] {key, Long.toString(count)}, - response -> castArray(handleArrayResponse(response), String.class)); + response -> castArray(handleArrayOrNullResponse(response), String.class)); } @Override @@ -1392,7 +1392,7 @@ public CompletableFuture lpopCount(@NonNull GlideString key, long return commandManager.submitNewCommand( LPop, new GlideString[] {key, gs(Long.toString(count))}, - response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index e58e3b5180..341ee307b3 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1695,6 +1695,7 @@ public void lpush_lpop_lrange_existing_non_existing_key(BaseClient client) { assertArrayEquals(new String[] {"value2", "value3"}, client.lpopCount(key, 2).get()); assertArrayEquals(new String[] {}, client.lrange("non_existing_key", 0, -1).get()); assertNull(client.lpop("non_existing_key").get()); + assertNull(client.lpopCount("non_existing_key", 2).get()); } @SneakyThrows @@ -1714,6 +1715,7 @@ public void lpush_lpop_lrange_binary_existing_non_existing_key(BaseClient client new GlideString[] {gs("value2"), gs("value3")}, client.lpopCount(key, 2).get()); assertArrayEquals(new GlideString[] {}, client.lrange(gs("non_existing_key"), 0, -1).get()); assertNull(client.lpop(gs("non_existing_key")).get()); + assertNull(client.lpopCount(gs("non_existing_key"), 2).get()); } @SneakyThrows diff --git a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java index 64d1faeed3..7a59533931 100644 --- a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java +++ b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java @@ -23,8 +23,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; @Timeout(10) // seconds public class StandaloneClientTests {