From 79a02dacc6c97bd8c571c3629052c1b46776ba98 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Thu, 23 Jan 2025 00:31:31 +0000 Subject: [PATCH 01/20] Implement Echo Connection Management Cluster Signed-off-by: EdricCua --- .../connection_management_cluster_commands.go | 14 ++++++++ go/api/connection_management_commands.go | 2 ++ go/api/glide_cluster_client.go | 34 ++++++++++++++++++ go/api/options/echo_options.go | 35 +++++++++++++++++++ go/integTest/standalone_commands_test.go | 12 +++++++ 5 files changed, 97 insertions(+) create mode 100644 go/api/connection_management_cluster_commands.go create mode 100644 go/api/options/echo_options.go diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go new file mode 100644 index 0000000000..0e9a016d7b --- /dev/null +++ b/go/api/connection_management_cluster_commands.go @@ -0,0 +1,14 @@ +// 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 { + EchoWithOptions(echoOptions *options.EchoOptions) (ClusterValue[interface{}], error) +} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index f9d4a1dada..89d8f16c71 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_commands.go @@ -51,4 +51,6 @@ type ConnectionManagementCommands interface { // // [valkey.io]: https://valkey.io/commands/echo/ Echo(message string) (Result[string], error) + + EchoWithOptions(echoOptions *options.EchoOptions) (string, error) } diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index cc672a91b5..22ccab5d1e 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -41,3 +41,37 @@ func (client *glideClusterClient) CustomCommand(args []string) (ClusterValue[int } return CreateClusterValue(data), nil } + +// Echoes the provided message back. +// +// Parameters: +// +// echoOptions - The EchoOptions type. +// +// Return value: +// +// Returns "PONG" or the copy of message. +// +// For example: +// +// route := config.SimpleNodeRoute(config.RandomRoute) +// options := options.NewEchoOptionsBuilder().SetRoute(route).SetMessage("Hello") +// result, err := client.EchoWithOptions(options) +// fmt.Println(result) // Output: "Hello" +// +// [valkey.io]: https://valkey.io/commands/echo/ +func (client *glideClusterClient) EchoWithOptions(opts *options.EchoOptions) (ClusterValue[interface{}], error) { + args, err := opts.ToArgs() + if err != nil { + return CreateEmptyClusterValue(), err + } + res, err := client.executeCommandWithRoute(C.Echo, args, opts.Route) + if err != nil { + return CreateEmptyClusterValue(), err + } + data, err := handleInterfaceResponse(res) + if err != nil { + return CreateEmptyClusterValue(), err + } + return CreateClusterValue(data), nil +} diff --git a/go/api/options/echo_options.go b/go/api/options/echo_options.go new file mode 100644 index 0000000000..24def35297 --- /dev/null +++ b/go/api/options/echo_options.go @@ -0,0 +1,35 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" +) + +type EchoOptions struct { + message string + Route config.Route +} + +func NewEchoOptionsBuilder() *EchoOptions { + return &EchoOptions{} +} + +func (echoOptions *EchoOptions) SetMessage(msg string) *EchoOptions { + echoOptions.message = msg + return echoOptions +} + +func (echoOptions *EchoOptions) SetRoute(route config.Route) *EchoOptions { + echoOptions.Route = route + return echoOptions +} + +func (opts *EchoOptions) ToArgs() ([]string, error) { + args := []string{} + + if opts.message != "" { + args = append(args, opts.message) + } + return args, nil +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 2d4a0ec31c..5b1eb541ef 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -384,3 +384,15 @@ func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_SuccessfulSortByWeightA assert.Equal(suite.T(), "item1", sortResult[3].Value()) assert.Equal(suite.T(), "item3", sortResult[5].Value()) } + +func (suite *GlideTestSuite) TestEchoWithOptions_WithRoute() { + client := suite.defaultClient() + options := options.NewEchoOptionsBuilder(). + SetRoute(config.SimpleNodeRoute(config.RandomRoute)) + + result, err := client.EchoWithOptions(options) + + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.RequestError{}, err) +} From 16939663ba68d7be9d9de1b84e6f0c23e89625c3 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Tue, 28 Jan 2025 08:31:15 +0000 Subject: [PATCH 02/20] Implement Echo Cluster Signed-off-by: EdricCua --- go/api/command_options.go | 68 +++++++++++++++++++ go/api/config/request_routing_config.go | 17 +++++ .../connection_management_cluster_commands.go | 6 +- go/api/glide_cluster_client.go | 50 +++++++++----- go/api/response_types.go | 47 +++++++------ go/integTest/cluster_commands_test.go | 68 +++++++++++++++++++ 6 files changed, 217 insertions(+), 39 deletions(-) diff --git a/go/api/command_options.go b/go/api/command_options.go index 8332afbd32..3f950033c9 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -5,6 +5,7 @@ package api import ( "strconv" + "github.com/valkey-io/valkey-glide/go/glide/api/config" "github.com/valkey-io/valkey-glide/go/glide/api/errors" "github.com/valkey-io/valkey-glide/go/glide/utils" ) @@ -394,3 +395,70 @@ func (opts *CopyOptions) toArgs() ([]string, error) { } return args, err } + +// Optional arguments for `Info` for standalone client +type EchoOptions struct { + // A list of [Section] values specifying which sections of information to retrieve. + // When no parameter is provided, [Section.Default] is assumed. + // Starting with server version 7.0.0 `INFO` command supports multiple sections. + Sections []Section +} + +type Section string + +const ( + // SERVER: General information about the server + Server Section = "server" + // CLIENTS: Client connections section + Clients Section = "clients" + // MEMORY: Memory consumption related information + Memory Section = "memory" + // PERSISTENCE: RDB and AOF related information + Persistence Section = "persistence" + // STATS: General statistics + Stats Section = "stats" + // REPLICATION: Master/replica replication information + Replication Section = "replication" + // CPU: CPU consumption statistics + Cpu Section = "cpu" + // COMMANDSTATS: Valkey command statistics + Commandstats Section = "commandstats" + // LATENCYSTATS: Valkey command latency percentile distribution statistics + Latencystats Section = "latencystats" + // SENTINEL: Valkey Sentinel section (only applicable to Sentinel instances) + Sentinel Section = "sentinel" + // CLUSTER: Valkey Cluster section + Cluster Section = "cluster" + // MODULES: Modules section + Modules Section = "modules" + // KEYSPACE: Database related statistics + Keyspace Section = "keyspace" + // ERRORSTATS: Valkey error statistics + Errorstats Section = "errorstats" + // ALL: Return all sections (excluding module generated ones) + All Section = "all" + // DEFAULT: Return only the default set of sections + Default Section = "default" + // EVERYTHING: Includes all and modules + Everything Section = "everything" +) + +// Optional arguments for `Echo` for cluster client +type ClusterEchoOptions struct { + *EchoOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by `Route`. + // The command will be routed to all primary nodes, unless `Route` is provided. + Route *config.Route +} + +func (opts *EchoOptions) toArgs() []string { + if opts == nil { + return []string{} + } + args := make([]string, 0, len(opts.Sections)) + for _, section := range opts.Sections { + args = append(args, string(section)) + } + return args +} diff --git a/go/api/config/request_routing_config.go b/go/api/config/request_routing_config.go index 1d0acc27d3..8bcb0221ab 100644 --- a/go/api/config/request_routing_config.go +++ b/go/api/config/request_routing_config.go @@ -18,8 +18,13 @@ import ( // - [config.ByAddressRoute] type Route interface { ToRoutesProtobuf() (*protobuf.Routes, error) + IsMultiNode() bool } +type notMultiNode struct{} + +func (*notMultiNode) IsMultiNode() bool { return false } + type SimpleNodeRoute int const ( @@ -47,6 +52,15 @@ func (simpleNodeRoute SimpleNodeRoute) ToRoutesProtobuf() (*protobuf.Routes, err return request, nil } +func (route SimpleNodeRoute) IsMultiNode() bool { + return route != RandomRoute +} + +func (snr SimpleNodeRoute) ToPtr() *Route { + a := Route(snr) + return &a +} + func mapSimpleNodeRoute(simpleNodeRoute SimpleNodeRoute) (protobuf.SimpleRoutes, error) { switch simpleNodeRoute { case AllNodes: @@ -86,6 +100,7 @@ func mapSlotType(slotType SlotType) (protobuf.SlotTypes, error) { type SlotIdRoute struct { slotType SlotType slotID int32 + notMultiNode } // - slotType: Defines type of the node being addressed. @@ -117,6 +132,7 @@ func (slotIdRoute *SlotIdRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { type SlotKeyRoute struct { slotType SlotType slotKey string + notMultiNode } // - slotType: Defines type of the node being addressed. @@ -146,6 +162,7 @@ func (slotKeyRoute *SlotKeyRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { type ByAddressRoute struct { host string port int32 + notMultiNode } // Create a route using hostname/address and port. diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go index 0e9a016d7b..17d1bd0e0b 100644 --- a/go/api/connection_management_cluster_commands.go +++ b/go/api/connection_management_cluster_commands.go @@ -2,13 +2,13 @@ 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 { - EchoWithOptions(echoOptions *options.EchoOptions) (ClusterValue[interface{}], error) + Echo(message string) (Result[string], error) + + EchoWithOptions(options ClusterEchoOptions) (ClusterValue[string], error) } diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index a403cea365..7e084604cc 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -70,36 +70,54 @@ func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[int return CreateClusterValue(data), nil } -// Echoes the provided message back. +// Echo the provided message back. +// The command will be routed a random node. // // Parameters: // -// echoOptions - The EchoOptions type. +// message - The provided message. // // Return value: // -// Returns "PONG" or the copy of message. +// A map where each address is the key and its corresponding node response is the information for the default sections. // // For example: // -// route := config.SimpleNodeRoute(config.RandomRoute) -// options := options.NewEchoOptionsBuilder().SetRoute(route).SetMessage("Hello") -// result, err := client.EchoWithOptions(options) -// fmt.Println(result) // Output: "Hello" +// response, err := clusterClient.Echo(opts) +// if err != nil { +// // handle error +// } +// for node, data := range response { +// fmt.Printf("%s node returned %s\n", node, data) +// } // // [valkey.io]: https://valkey.io/commands/echo/ -func (client *glideClusterClient) EchoWithOptions(opts *options.EchoOptions) (ClusterValue[interface{}], error) { - args, err := opts.ToArgs() - if err != nil { - return CreateEmptyClusterValue(), err +func (client *GlideClusterClient) EchoWithOptions(options ClusterEchoOptions) (ClusterValue[string], error) { + if options.Route == nil { + response, err := client.executeCommand(C.Echo, options.toArgs()) + if err != nil { + return createEmptyClusterValue[string](), err + } + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterMultiValue[string](data), nil } - res, err := client.executeCommandWithRoute(C.Echo, args, opts.Route) + response, err := client.executeCommandWithRoute(C.Echo, options.toArgs(), *options.Route) if err != nil { - return CreateEmptyClusterValue(), err + return createEmptyClusterValue[string](), err } - data, err := handleInterfaceResponse(res) + if (*options.Route).IsMultiNode() { + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterMultiValue[string](data), nil + } + data, err := handleStringResponse(response) if err != nil { - return CreateEmptyClusterValue(), err + return createEmptyClusterValue[string](), err } - return CreateClusterValue(data), nil + return createClusterSingleValue[string](data), nil } diff --git a/go/api/response_types.go b/go/api/response_types.go index 84de6aed7f..2f983d769f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -88,7 +88,7 @@ const ( // Enum-like structure which stores either a single-node response or multi-node response. // Multi-node response stored in a map, where keys are hostnames or ":" strings. // -// For example: +// Example: // // // Command failed: // value, err := clusterClient.CustomCommand(args) @@ -96,25 +96,32 @@ const ( // err != nil: true // // // Command returns response from multiple nodes: -// value, _ := clusterClient.info() -// node, nodeResponse := range value.Value().(map[string]interface{}) { -// response := nodeResponse.(string) +// value, _ := clusterClient.Info() +// for node, nodeResponse := range value.MultiValue() { +// response := nodeResponse // // `node` stores cluster node IP/hostname, `response` stores the command output from that node // } // // // Command returns a response from single node: -// value, _ := clusterClient.infoWithRoute(Random{}) -// response := value.Value().(string) +// value, _ := clusterClient.InfoWithOptions(api.ClusterInfoOptions{InfoOptions: nil, Route: api.RandomRoute.ToPtr()}) +// response := value.SingleValue() // // `response` stores the command output from a cluster node type ClusterValue[T any] struct { valueType ValueType - value Result[T] + value Result[any] } -func (value ClusterValue[T]) Value() T { - return value.value.Value() +// Get the single value stored (value returned by a single cluster node). +func (value ClusterValue[T]) SingleValue() T { + return value.value.Value().(T) } +// Get the multi value stored (value returned by multiple cluster nodes). +func (value ClusterValue[T]) MultiValue() map[string]T { + return value.value.Value().(map[string]T) +} + +// Get the value type func (value ClusterValue[T]) ValueType() ValueType { return value.valueType } @@ -131,33 +138,33 @@ func (value ClusterValue[T]) IsEmpty() bool { return value.value.IsNil() } -func CreateClusterValue[T any](data T) ClusterValue[T] { +func createClusterValue[T any](data any) ClusterValue[T] { switch any(data).(type) { case map[string]interface{}: - return CreateClusterMultiValue(data) + return createClusterMultiValue(data.(map[string]T)) default: - return CreateClusterSingleValue(data) + return createClusterSingleValue(data.(T)) } } -func CreateClusterSingleValue[T any](data T) ClusterValue[T] { +func createClusterSingleValue[T any](data T) ClusterValue[T] { return ClusterValue[T]{ valueType: SingleValue, - value: Result[T]{val: data, isNil: false}, + value: Result[any]{val: data, isNil: false}, } } -func CreateClusterMultiValue[T any](data T) ClusterValue[T] { +func createClusterMultiValue[T any](data map[string]T) ClusterValue[T] { return ClusterValue[T]{ valueType: MultiValue, - value: Result[T]{val: data, isNil: false}, + value: Result[any]{val: data, isNil: false}, } } -func CreateEmptyClusterValue() ClusterValue[interface{}] { - var empty interface{} - return ClusterValue[interface{}]{ - value: Result[interface{}]{val: empty, isNil: true}, +func createEmptyClusterValue[T any]() ClusterValue[T] { + var empty T + return ClusterValue[T]{ + value: Result[any]{val: empty, isNil: true}, } } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 142f0cf273..0d754efe22 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -27,3 +27,71 @@ func (suite *GlideTestSuite) TestClusterCustomCommandEcho() { // ECHO is routed to a single random node assert.Equal(suite.T(), "GO GLIDE GO", result.Value().(string)) } + +func (suite *GlideTestSuite) TestEchoCluster() { + DEFAULT_INFO_SECTIONS := []string{ + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Errorstats", + "Cluster", + "Keyspace", + } + + client := suite.defaultClusterClient() + t := suite.T() + + // info with option or with multiple options without route + sections := []api.Section{api.Cpu} + if suite.serverVersion >= "7.0.0" { + sections = append(sections, api.Memory) + } + opts := api.ClusterEchoOptions{ + EchoOptions: &api.EchoOptions{Sections: sections}, + Route: nil, + } + response, err := client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) + for _, info := range response.MultiValue() { + for _, section := range sections { + assert.Contains(t, strings.ToLower(info), strings.ToLower("# "+string(section)), "Section "+section+" is missing") + } + } + + // same sections with random route + opts = api.ClusterEchoOptions{ + EchoOptions: &api.EchoOptions{Sections: sections}, + Route: config.RandomRoute.ToPtr(), + } + response, err = client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsSingleValue()) + for _, section := range sections { + assert.Contains( + t, + strings.ToLower(response.SingleValue()), + strings.ToLower("# "+string(section)), + "Section "+section+" is missing", + ) + } + + // default sections, multi node route + opts = api.ClusterEchoOptions{ + EchoOptions: nil, + Route: config.AllPrimaries.ToPtr(), + } + response, err = client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) + for _, info := range response.MultiValue() { + for _, section := range DEFAULT_INFO_SECTIONS { + assert.Contains(t, info, "# "+section, "Section "+section+" is missing") + } + } +} From 8a34022cbd1118fef203d8eda6f6eb215708bb4b Mon Sep 17 00:00:00 2001 From: EdricCua Date: Tue, 28 Jan 2025 08:46:58 +0000 Subject: [PATCH 03/20] Implement Echo Cluster Signed-off-by: EdricCua --- go/api/connection_management_commands.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index 2a92bb77c5..480b85af91 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_commands.go @@ -13,6 +13,4 @@ type ConnectionManagementCommands interface { PingWithMessage(message string) (string, error) Echo(message string) (Result[string], error) - - EchoWithOptions(echoOptions *options.EchoOptions) (string, error) } From 8f8c70295b6c4b70ff22afdfd95506048a3ae240 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Tue, 28 Jan 2025 20:49:35 +0000 Subject: [PATCH 04/20] Implement Echo Cluster Signed-off-by: EdricCua --- go/api/response_types.go | 47 +++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/go/api/response_types.go b/go/api/response_types.go index 2f983d769f..84de6aed7f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -88,7 +88,7 @@ const ( // Enum-like structure which stores either a single-node response or multi-node response. // Multi-node response stored in a map, where keys are hostnames or ":" strings. // -// Example: +// For example: // // // Command failed: // value, err := clusterClient.CustomCommand(args) @@ -96,32 +96,25 @@ const ( // err != nil: true // // // Command returns response from multiple nodes: -// value, _ := clusterClient.Info() -// for node, nodeResponse := range value.MultiValue() { -// response := nodeResponse +// value, _ := clusterClient.info() +// node, nodeResponse := range value.Value().(map[string]interface{}) { +// response := nodeResponse.(string) // // `node` stores cluster node IP/hostname, `response` stores the command output from that node // } // // // Command returns a response from single node: -// value, _ := clusterClient.InfoWithOptions(api.ClusterInfoOptions{InfoOptions: nil, Route: api.RandomRoute.ToPtr()}) -// response := value.SingleValue() +// value, _ := clusterClient.infoWithRoute(Random{}) +// response := value.Value().(string) // // `response` stores the command output from a cluster node type ClusterValue[T any] struct { valueType ValueType - value Result[any] + value Result[T] } -// Get the single value stored (value returned by a single cluster node). -func (value ClusterValue[T]) SingleValue() T { - return value.value.Value().(T) +func (value ClusterValue[T]) Value() T { + return value.value.Value() } -// Get the multi value stored (value returned by multiple cluster nodes). -func (value ClusterValue[T]) MultiValue() map[string]T { - return value.value.Value().(map[string]T) -} - -// Get the value type func (value ClusterValue[T]) ValueType() ValueType { return value.valueType } @@ -138,33 +131,33 @@ func (value ClusterValue[T]) IsEmpty() bool { return value.value.IsNil() } -func createClusterValue[T any](data any) ClusterValue[T] { +func CreateClusterValue[T any](data T) ClusterValue[T] { switch any(data).(type) { case map[string]interface{}: - return createClusterMultiValue(data.(map[string]T)) + return CreateClusterMultiValue(data) default: - return createClusterSingleValue(data.(T)) + return CreateClusterSingleValue(data) } } -func createClusterSingleValue[T any](data T) ClusterValue[T] { +func CreateClusterSingleValue[T any](data T) ClusterValue[T] { return ClusterValue[T]{ valueType: SingleValue, - value: Result[any]{val: data, isNil: false}, + value: Result[T]{val: data, isNil: false}, } } -func createClusterMultiValue[T any](data map[string]T) ClusterValue[T] { +func CreateClusterMultiValue[T any](data T) ClusterValue[T] { return ClusterValue[T]{ valueType: MultiValue, - value: Result[any]{val: data, isNil: false}, + value: Result[T]{val: data, isNil: false}, } } -func createEmptyClusterValue[T any]() ClusterValue[T] { - var empty T - return ClusterValue[T]{ - value: Result[any]{val: empty, isNil: true}, +func CreateEmptyClusterValue() ClusterValue[interface{}] { + var empty interface{} + return ClusterValue[interface{}]{ + value: Result[interface{}]{val: empty, isNil: true}, } } From f053d399d80b67d7796961ff3994aecdba0c619d Mon Sep 17 00:00:00 2001 From: EdricCua Date: Tue, 28 Jan 2025 21:09:58 +0000 Subject: [PATCH 05/20] Implement Echo Cluster Signed-off-by: EdricCua --- go/api/command_options.go | 2 +- .../connection_management_cluster_commands.go | 2 -- go/api/glide_cluster_client.go | 28 +++++++++++-------- go/api/options/route_options.go | 9 ++++++ 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 go/api/options/route_options.go diff --git a/go/api/command_options.go b/go/api/command_options.go index 3f950033c9..73c5897db0 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -452,7 +452,7 @@ type ClusterEchoOptions struct { Route *config.Route } -func (opts *EchoOptions) toArgs() []string { +func (opts *EchoOptions) ToArgs() []string { if opts == nil { return []string{} } diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go index 17d1bd0e0b..727e44fd09 100644 --- a/go/api/connection_management_cluster_commands.go +++ b/go/api/connection_management_cluster_commands.go @@ -8,7 +8,5 @@ package api // // [valkey.io]: https://valkey.io/commands/#connection type ConnectionManagementClusterCommands interface { - Echo(message string) (Result[string], error) - EchoWithOptions(options ClusterEchoOptions) (ClusterValue[string], error) } diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 7e084604cc..ede9eb2897 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -6,6 +6,10 @@ package api // #include "../lib.h" import "C" +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/options" +) + // GlideClusterClient interface compliance check. var _ GlideClusterClientCommands = (*GlideClusterClient)(nil) @@ -13,6 +17,7 @@ var _ GlideClusterClientCommands = (*GlideClusterClient)(nil) type GlideClusterClientCommands interface { BaseClient GenericClusterCommands + ServerManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. @@ -83,7 +88,7 @@ func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[int // // For example: // -// response, err := clusterClient.Echo(opts) +// response, err := clusterClient.EchoWithOptions(opts) // if err != nil { // // handle error // } @@ -92,32 +97,33 @@ func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[int // } // // [valkey.io]: https://valkey.io/commands/echo/ -func (client *GlideClusterClient) EchoWithOptions(options ClusterEchoOptions) (ClusterValue[string], error) { +func (client *GlideClusterClient) EchoWithOptions(opts *options.EchoOptions) (ClusterValue[string], error) { + args, err := opts.ToArgs() if options.Route == nil { - response, err := client.executeCommand(C.Echo, options.toArgs()) + response, err := client.executeCommand(C.Echo, args) if err != nil { - return createEmptyClusterValue[string](), err + return CreateEmptyClusterValue[string](), err } data, err := handleStringToStringMapResponse(response) if err != nil { - return createEmptyClusterValue[string](), err + return CreateEmptyClusterValue[string](), err } - return createClusterMultiValue[string](data), nil + return CreateClusterMultiValue[string](data), nil } - response, err := client.executeCommandWithRoute(C.Echo, options.toArgs(), *options.Route) + response, err := client.executeCommandWithRoute(C.Echo, args, *options.Route) if err != nil { - return createEmptyClusterValue[string](), err + return CreateEmptyClusterValue[string](), err } if (*options.Route).IsMultiNode() { data, err := handleStringToStringMapResponse(response) if err != nil { - return createEmptyClusterValue[string](), err + return CreateEmptyClusterValue[string](), err } - return createClusterMultiValue[string](data), nil + return CreateClusterMultiValue[string](data), nil } data, err := handleStringResponse(response) if err != nil { - return createEmptyClusterValue[string](), err + return CreateEmptyClusterValue[string](), err } return createClusterSingleValue[string](data), nil } diff --git a/go/api/options/route_options.go b/go/api/options/route_options.go new file mode 100644 index 0000000000..1f1cbd2b20 --- /dev/null +++ b/go/api/options/route_options.go @@ -0,0 +1,9 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + +type RouteOption struct { + Route config.Route +} From 37e16f46a097f3aab1868723964161cfe7f1cb1a Mon Sep 17 00:00:00 2001 From: EdricCua Date: Tue, 28 Jan 2025 22:42:46 +0000 Subject: [PATCH 06/20] Fix Echo cluster command Signed-off-by: EdricCua --- .../connection_management_cluster_commands.go | 4 +- go/api/glide_cluster_client.go | 35 ++----- go/api/options/echo_options.go | 34 +++---- go/integTest/cluster_commands_test.go | 97 +++++++------------ go/integTest/standalone_commands_test.go | 12 --- 5 files changed, 65 insertions(+), 117 deletions(-) diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go index 727e44fd09..5b6129aec7 100644 --- a/go/api/connection_management_cluster_commands.go +++ b/go/api/connection_management_cluster_commands.go @@ -2,11 +2,13 @@ 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 { - EchoWithOptions(options ClusterEchoOptions) (ClusterValue[string], error) + EchoWithOptions(echoOptions options.ClusterEchoOptions) (string, error) } diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index ede9eb2897..d7aa63ca0c 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -17,7 +17,6 @@ var _ GlideClusterClientCommands = (*GlideClusterClient)(nil) type GlideClusterClientCommands interface { BaseClient GenericClusterCommands - ServerManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. @@ -97,33 +96,19 @@ func (client *GlideClusterClient) CustomCommand(args []string) (ClusterValue[int // } // // [valkey.io]: https://valkey.io/commands/echo/ -func (client *GlideClusterClient) EchoWithOptions(opts *options.EchoOptions) (ClusterValue[string], error) { - args, err := opts.ToArgs() - if options.Route == nil { - response, err := client.executeCommand(C.Echo, args) +func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (string, error) { + if echoOptions.Route == nil { + response, err := client.executeCommand(C.Echo, echoOptions.ToArgs()) if err != nil { - return CreateEmptyClusterValue[string](), err + return defaultStringResponse, err } - data, err := handleStringToStringMapResponse(response) - if err != nil { - return CreateEmptyClusterValue[string](), err - } - return CreateClusterMultiValue[string](data), nil + return handleStringResponse(response) } - response, err := client.executeCommandWithRoute(C.Echo, args, *options.Route) - if err != nil { - return CreateEmptyClusterValue[string](), err - } - if (*options.Route).IsMultiNode() { - data, err := handleStringToStringMapResponse(response) - if err != nil { - return CreateEmptyClusterValue[string](), err - } - return CreateClusterMultiValue[string](data), nil - } - data, err := handleStringResponse(response) + + response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), *echoOptions.Route) if err != nil { - return CreateEmptyClusterValue[string](), err + return defaultStringResponse, err } - return createClusterSingleValue[string](data), nil + + return handleStringResponse(response) } diff --git a/go/api/options/echo_options.go b/go/api/options/echo_options.go index 24def35297..4f2cd784be 100644 --- a/go/api/options/echo_options.go +++ b/go/api/options/echo_options.go @@ -6,30 +6,26 @@ import ( "github.com/valkey-io/valkey-glide/go/glide/api/config" ) +// Optional arguments for `Echo` for standalone client type EchoOptions struct { - message string - Route config.Route + Message string } -func NewEchoOptionsBuilder() *EchoOptions { - return &EchoOptions{} +// Optional arguments for `Echo` for cluster client +type ClusterEchoOptions struct { + *EchoOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by *Route*. + Route *config.Route } -func (echoOptions *EchoOptions) SetMessage(msg string) *EchoOptions { - echoOptions.message = msg - return echoOptions -} - -func (echoOptions *EchoOptions) SetRoute(route config.Route) *EchoOptions { - echoOptions.Route = route - return echoOptions -} - -func (opts *EchoOptions) ToArgs() ([]string, error) { +func (opts *EchoOptions) ToArgs() []string { + if opts == nil { + return []string{} + } args := []string{} - - if opts.message != "" { - args = append(args, opts.message) + if opts.Message != "" { + args = append(args, opts.Message) } - return args, nil + return args } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 0d754efe22..3984231d16 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api/config" + "github.com/valkey-io/valkey-glide/go/glide/api/options" ) func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { @@ -27,71 +29,46 @@ func (suite *GlideTestSuite) TestClusterCustomCommandEcho() { // ECHO is routed to a single random node assert.Equal(suite.T(), "GO GLIDE GO", result.Value().(string)) } - -func (suite *GlideTestSuite) TestEchoCluster() { - DEFAULT_INFO_SECTIONS := []string{ - "Server", - "Clients", - "Memory", - "Persistence", - "Stats", - "Replication", - "CPU", - "Modules", - "Errorstats", - "Cluster", - "Keyspace", +func (suite *GlideTestSuite) TestEchoWithOptionsNoRoute() { + client := suite.defaultClusterClient() + options := options.ClusterEchoOptions{ + EchoOptions: &options.EchoOptions{ + Message: "hello", + }, + Route: nil, } - client := suite.defaultClusterClient() - t := suite.T() + result, err := client.EchoWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} - // info with option or with multiple options without route - sections := []api.Section{api.Cpu} - if suite.serverVersion >= "7.0.0" { - sections = append(sections, api.Memory) - } - opts := api.ClusterEchoOptions{ - EchoOptions: &api.EchoOptions{Sections: sections}, - Route: nil, - } - response, err := client.EchoWithOptions(opts) - assert.NoError(t, err) - assert.True(t, response.IsMultiValue()) - for _, info := range response.MultiValue() { - for _, section := range sections { - assert.Contains(t, strings.ToLower(info), strings.ToLower("# "+string(section)), "Section "+section+" is missing") - } +func (suite *GlideTestSuite) TestEchoWithOptionsWithRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.AllNodes) + options := options.ClusterEchoOptions{ + EchoOptions: &options.EchoOptions{ + Message: "hello", + }, + Route: &route, } - // same sections with random route - opts = api.ClusterEchoOptions{ - EchoOptions: &api.EchoOptions{Sections: sections}, - Route: config.RandomRoute.ToPtr(), - } - response, err = client.EchoWithOptions(opts) - assert.NoError(t, err) - assert.True(t, response.IsSingleValue()) - for _, section := range sections { - assert.Contains( - t, - strings.ToLower(response.SingleValue()), - strings.ToLower("# "+string(section)), - "Section "+section+" is missing", - ) - } + result, err := client.EchoWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} - // default sections, multi node route - opts = api.ClusterEchoOptions{ - EchoOptions: nil, - Route: config.AllPrimaries.ToPtr(), - } - response, err = client.EchoWithOptions(opts) - assert.NoError(t, err) - assert.True(t, response.IsMultiValue()) - for _, info := range response.MultiValue() { - for _, section := range DEFAULT_INFO_SECTIONS { - assert.Contains(t, info, "# "+section, "Section "+section+" is missing") - } +func (suite *GlideTestSuite) TestEchoWithOptionsInvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) + options := options.ClusterEchoOptions{ + EchoOptions: &options.EchoOptions{ + Message: "hello", + }, + Route: &invalidRoute, } + + result, err := client.EchoWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Empty(suite.T(), result) } diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 9c0645a80c..6b7b0908c0 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -392,15 +392,3 @@ func (suite *GlideTestSuite) TestDBSize() { assert.Nil(suite.T(), err) assert.Greater(suite.T(), result, int64(0)) } - -func (suite *GlideTestSuite) TestEchoWithOptions_WithRoute() { - client := suite.defaultClient() - options := options.NewEchoOptionsBuilder(). - SetRoute(config.SimpleNodeRoute(config.RandomRoute)) - - result, err := client.EchoWithOptions(options) - - assert.NotNil(suite.T(), err) - assert.Equal(suite.T(), "", result) - assert.IsType(suite.T(), &errors.RequestError{}, err) -} From 238548780038439e4e40d96f9a2421da78273c07 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Wed, 29 Jan 2025 00:16:26 +0000 Subject: [PATCH 07/20] Fix merge conflict Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index b0ec5f35fd..9caf6fc841 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -18,6 +18,7 @@ type GlideClusterClientCommands interface { BaseClient GenericClusterCommands ServerManagementClusterCommands + ConnectionManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. From 5fbc2bb8c3c4c717e45943cbbc2c44f585b078e3 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Wed, 29 Jan 2025 00:23:52 +0000 Subject: [PATCH 08/20] Fix merge conflict Signed-off-by: EdricCua --- go/api/command_options.go | 67 ---------------------------------- go/api/glide_cluster_client.go | 1 - 2 files changed, 68 deletions(-) diff --git a/go/api/command_options.go b/go/api/command_options.go index 4660a90044..23bc686e1c 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -462,70 +462,3 @@ func (opts *CopyOptions) toArgs() ([]string, error) { } return args, err } - -// Optional arguments for `Info` for standalone client -type EchoOptions struct { - // A list of [Section] values specifying which sections of information to retrieve. - // When no parameter is provided, [Section.Default] is assumed. - // Starting with server version 7.0.0 `INFO` command supports multiple sections. - Sections []Section -} - -type Section string - -const ( - // SERVER: General information about the server - Server Section = "server" - // CLIENTS: Client connections section - Clients Section = "clients" - // MEMORY: Memory consumption related information - Memory Section = "memory" - // PERSISTENCE: RDB and AOF related information - Persistence Section = "persistence" - // STATS: General statistics - Stats Section = "stats" - // REPLICATION: Master/replica replication information - Replication Section = "replication" - // CPU: CPU consumption statistics - Cpu Section = "cpu" - // COMMANDSTATS: Valkey command statistics - Commandstats Section = "commandstats" - // LATENCYSTATS: Valkey command latency percentile distribution statistics - Latencystats Section = "latencystats" - // SENTINEL: Valkey Sentinel section (only applicable to Sentinel instances) - Sentinel Section = "sentinel" - // CLUSTER: Valkey Cluster section - Cluster Section = "cluster" - // MODULES: Modules section - Modules Section = "modules" - // KEYSPACE: Database related statistics - Keyspace Section = "keyspace" - // ERRORSTATS: Valkey error statistics - Errorstats Section = "errorstats" - // ALL: Return all sections (excluding module generated ones) - All Section = "all" - // DEFAULT: Return only the default set of sections - Default Section = "default" - // EVERYTHING: Includes all and modules - Everything Section = "everything" -) - -// Optional arguments for `Echo` for cluster client -type ClusterEchoOptions struct { - *EchoOptions - // Specifies the routing configuration for the command. - // The client will route the command to the nodes defined by `Route`. - // The command will be routed to all primary nodes, unless `Route` is provided. - Route *config.Route -} - -func (opts *EchoOptions) ToArgs() []string { - if opts == nil { - return []string{} - } - args := make([]string, 0, len(opts.Sections)) - for _, section := range opts.Sections { - args = append(args, string(section)) - } - return args -} diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 9caf6fc841..b0ec5f35fd 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -18,7 +18,6 @@ type GlideClusterClientCommands interface { BaseClient GenericClusterCommands ServerManagementClusterCommands - ConnectionManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. From 3eea2419927b3bb8eb31d84ac548d60f2b1d7c5c Mon Sep 17 00:00:00 2001 From: EdricCua Date: Wed, 29 Jan 2025 00:33:27 +0000 Subject: [PATCH 09/20] Fix merge conflict Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 1 + go/integTest/cluster_commands_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index b0ec5f35fd..9caf6fc841 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -18,6 +18,7 @@ type GlideClusterClientCommands interface { BaseClient GenericClusterCommands ServerManagementClusterCommands + ConnectionManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 0d98d156e9..d5622ee0e0 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() { From c0f5646bf796e432367f4a9051df91b24c8be34f Mon Sep 17 00:00:00 2001 From: EdricCua Date: Wed, 29 Jan 2025 06:30:42 +0000 Subject: [PATCH 10/20] Fix test Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 29 ++++++++++----- go/integTest/cluster_commands_test.go | 52 ++++++++++++++------------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 9caf6fc841..6f24f26796 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -188,19 +188,32 @@ func (client *GlideClusterClient) InfoWithOptions(options ClusterInfoOptions) (C // } // // [valkey.io]: https://valkey.io/commands/echo/ -func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (string, error) { +func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) { if echoOptions.Route == nil { - response, err := client.executeCommand(C.Echo, echoOptions.ToArgs()) + response, err := client.executeCommand(C.Info, echoOptions.ToArgs()) if err != nil { - return defaultStringResponse, err + return createEmptyClusterValue[string](), err + } + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err } - return handleStringResponse(response) + return createClusterMultiValue[string](data), nil } - response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), *echoOptions.Route) if err != nil { - return defaultStringResponse, err + return createEmptyClusterValue[string](), err } - - return handleStringResponse(response) + if (*echoOptions.Route).IsMultiNode() { + data, err := handleStringToStringMapResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterMultiValue[string](data), nil + } + data, err := handleStringResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterSingleValue[string](data), nil } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index d5622ee0e0..e4a7452acc 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -3,6 +3,7 @@ package integTest import ( + "fmt" "strings" "github.com/stretchr/testify/assert" @@ -107,46 +108,47 @@ func (suite *GlideTestSuite) TestInfoCluster() { } } } -func (suite *GlideTestSuite) TestEchoWithOptionsNoRoute() { + +func (suite *GlideTestSuite) TestEchoCluster() { client := suite.defaultClusterClient() - options := options.ClusterEchoOptions{ + t := suite.T() + // echo with option or with multiple options without route + opts := options.ClusterEchoOptions{ EchoOptions: &options.EchoOptions{ Message: "hello", }, Route: nil, } + response, err := client.EchoWithOptions(opts) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) - result, err := client.EchoWithOptions(options) - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "hello", result) -} - -func (suite *GlideTestSuite) TestEchoWithOptionsWithRoute() { - client := suite.defaultClusterClient() - route := config.Route(config.AllNodes) - options := options.ClusterEchoOptions{ + // same sections with random route + route := config.Route(*config.RandomRoute.ToPtr()) + opts = options.ClusterEchoOptions{ EchoOptions: &options.EchoOptions{ Message: "hello", }, Route: &route, } + response, err = client.EchoWithOptions(opts) + fmt.Println("response: ", response) + assert.NoError(t, err) + assert.True(t, response.IsSingleValue()) - result, err := client.EchoWithOptions(options) - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "hello", result) -} - -func (suite *GlideTestSuite) TestEchoWithOptionsInvalidRoute() { - client := suite.defaultClusterClient() - invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) - options := options.ClusterEchoOptions{ + // default sections, multi node route + routeMultiNode := config.Route(*config.AllPrimaries.ToPtr()) + opts = options.ClusterEchoOptions{ EchoOptions: &options.EchoOptions{ Message: "hello", }, - Route: &invalidRoute, + Route: &routeMultiNode, + } + response, err = client.EchoWithOptions(opts) + fmt.Println("response: ", response) + assert.NoError(t, err) + assert.True(t, response.IsMultiValue()) + for _, messages := range response.MultiValue() { + assert.Contains(t, strings.ToLower(messages), strings.ToLower("hello")) } - - result, err := client.EchoWithOptions(options) - assert.NotNil(suite.T(), err) - assert.Empty(suite.T(), result) } From 01c7ec7360ea4b4cd074f3bf9b75c4529dda25d9 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Wed, 29 Jan 2025 06:39:51 +0000 Subject: [PATCH 11/20] Fix test Signed-off-by: EdricCua --- go/api/connection_management_cluster_commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go index 5b6129aec7..cabe2bdd1b 100644 --- a/go/api/connection_management_cluster_commands.go +++ b/go/api/connection_management_cluster_commands.go @@ -10,5 +10,5 @@ import "github.com/valkey-io/valkey-glide/go/glide/api/options" // // [valkey.io]: https://valkey.io/commands/#connection type ConnectionManagementClusterCommands interface { - EchoWithOptions(echoOptions options.ClusterEchoOptions) (string, error) + EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) } From a6f1f75f76ffacaab7b2547bf11849853436540b Mon Sep 17 00:00:00 2001 From: EdricCua Date: Thu, 30 Jan 2025 04:25:58 +0000 Subject: [PATCH 12/20] Addressed review comments Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 17 +++-------------- go/api/options/echo_options.go | 2 +- go/api/options/route_options.go | 3 +++ go/integTest/cluster_commands_test.go | 6 +++--- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 90d04f8ec1..3ef09078ab 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -268,7 +268,7 @@ func (client *GlideClusterClient) PingWithOptions(pingOptions options.ClusterPin } // Echo the provided message back. -// The command will be routed a random node. +// The command will be routed a random node, unless `Route` in `echoOptions` is provided. // // Parameters: // @@ -290,22 +290,11 @@ func (client *GlideClusterClient) PingWithOptions(pingOptions options.ClusterPin // // [valkey.io]: https://valkey.io/commands/echo/ func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) { - if echoOptions.Route == nil { - response, err := client.executeCommand(C.Info, echoOptions.ToArgs()) - if err != nil { - return createEmptyClusterValue[string](), err - } - data, err := handleStringToStringMapResponse(response) - if err != nil { - return createEmptyClusterValue[string](), err - } - return createClusterMultiValue[string](data), nil - } - response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), *echoOptions.Route) + response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), *echoOptions.RouteOption) if err != nil { return createEmptyClusterValue[string](), err } - if (*echoOptions.Route).IsMultiNode() { + if (*echoOptions.RouteOption).IsMultiNode() { data, err := handleStringToStringMapResponse(response) if err != nil { return createEmptyClusterValue[string](), err diff --git a/go/api/options/echo_options.go b/go/api/options/echo_options.go index 4f2cd784be..3163e7b701 100644 --- a/go/api/options/echo_options.go +++ b/go/api/options/echo_options.go @@ -16,7 +16,7 @@ type ClusterEchoOptions struct { *EchoOptions // Specifies the routing configuration for the command. // The client will route the command to the nodes defined by *Route*. - Route *config.Route + RouteOption *config.Route } func (opts *EchoOptions) ToArgs() []string { diff --git a/go/api/options/route_options.go b/go/api/options/route_options.go index 1f1cbd2b20..177f470529 100644 --- a/go/api/options/route_options.go +++ b/go/api/options/route_options.go @@ -4,6 +4,9 @@ package options import "github.com/valkey-io/valkey-glide/go/glide/api/config" +// An extension to command option types with Routes type RouteOption struct { + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by `route`. Route config.Route } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 90bfd265f6..a2c618a1fb 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -196,7 +196,7 @@ func (suite *GlideTestSuite) TestEchoCluster() { EchoOptions: &options.EchoOptions{ Message: "hello", }, - Route: nil, + RouteOption: nil, } response, err := client.EchoWithOptions(opts) assert.NoError(t, err) @@ -208,7 +208,7 @@ func (suite *GlideTestSuite) TestEchoCluster() { EchoOptions: &options.EchoOptions{ Message: "hello", }, - Route: &route, + RouteOption: &route, } response, err = client.EchoWithOptions(opts) fmt.Println("response: ", response) @@ -221,7 +221,7 @@ func (suite *GlideTestSuite) TestEchoCluster() { EchoOptions: &options.EchoOptions{ Message: "hello", }, - Route: &routeMultiNode, + RouteOption: &routeMultiNode, } response, err = client.EchoWithOptions(opts) fmt.Println("response: ", response) From c02b0e570d51f098ed1f7f504874ec5d48754bca Mon Sep 17 00:00:00 2001 From: EdricCua Date: Thu, 30 Jan 2025 04:35:31 +0000 Subject: [PATCH 13/20] Addressed review comments Signed-off-by: EdricCua --- go/api/connection_management_cluster_commands.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go index 7f5b92461e..142eecdc18 100644 --- a/go/api/connection_management_cluster_commands.go +++ b/go/api/connection_management_cluster_commands.go @@ -13,18 +13,6 @@ type ConnectionManagementClusterCommands interface { Ping() (string, error) PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) -} -// 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 { EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) } From 4cd2d84d607d8e19683ff8611ae2bcadb39727be Mon Sep 17 00:00:00 2001 From: EdricCua Date: Thu, 30 Jan 2025 21:05:36 +0000 Subject: [PATCH 14/20] Fix review comment Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 16 ++++++++++++++-- go/api/options/echo_options.go | 6 +----- go/api/options/route_options.go | 9 --------- go/integTest/cluster_commands_test.go | 14 ++++++-------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 701038f719..20277eb705 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -322,11 +322,23 @@ func (client *GlideClusterClient) TimeWithOptions(opts options.RouteOption) (Clu // // [valkey.io]: https://valkey.io/commands/echo/ func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) { - response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), *echoOptions.RouteOption) + if echoOptions.RouteOption.Route == nil { + response, err := client.executeCommand(C.Echo, echoOptions.ToArgs()) + if err != nil { + return createEmptyClusterValue[string](), err + } + data, err := handleStringResponse(response) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterSingleValue[string](data), nil + } + response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), + echoOptions.RouteOption.Route) if err != nil { return createEmptyClusterValue[string](), err } - if (*echoOptions.RouteOption).IsMultiNode() { + if (echoOptions.RouteOption.Route).IsMultiNode() { data, err := handleStringToStringMapResponse(response) if err != nil { return createEmptyClusterValue[string](), err diff --git a/go/api/options/echo_options.go b/go/api/options/echo_options.go index 3163e7b701..383f46a6ce 100644 --- a/go/api/options/echo_options.go +++ b/go/api/options/echo_options.go @@ -2,10 +2,6 @@ package options -import ( - "github.com/valkey-io/valkey-glide/go/glide/api/config" -) - // Optional arguments for `Echo` for standalone client type EchoOptions struct { Message string @@ -16,7 +12,7 @@ type ClusterEchoOptions struct { *EchoOptions // Specifies the routing configuration for the command. // The client will route the command to the nodes defined by *Route*. - RouteOption *config.Route + *RouteOption } func (opts *EchoOptions) ToArgs() []string { diff --git a/go/api/options/route_options.go b/go/api/options/route_options.go index 013e166dcf..177f470529 100644 --- a/go/api/options/route_options.go +++ b/go/api/options/route_options.go @@ -4,15 +4,6 @@ package options import "github.com/valkey-io/valkey-glide/go/glide/api/config" -type RouteOption struct { - Route config.Route -} -// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - -package options - -import "github.com/valkey-io/valkey-glide/go/glide/api/config" - // An extension to command option types with Routes type RouteOption struct { // Specifies the routing configuration for the command. diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 4b3e07b083..a6a7da5d3c 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -3,7 +3,6 @@ package integTest import ( - "fmt" "strings" "github.com/stretchr/testify/assert" @@ -249,19 +248,20 @@ func (suite *GlideTestSuite) TestTimeWithInvalidRoute() { func (suite *GlideTestSuite) TestEchoCluster() { client := suite.defaultClusterClient() t := suite.T() + // echo with option or with multiple options without route opts := options.ClusterEchoOptions{ EchoOptions: &options.EchoOptions{ Message: "hello", }, - RouteOption: nil, + RouteOption: &options.RouteOption{Route: nil}, } response, err := client.EchoWithOptions(opts) assert.NoError(t, err) - assert.True(t, response.IsMultiValue()) + assert.True(t, response.IsSingleValue()) // same sections with random route - route := config.Route(*config.RandomRoute.ToPtr()) + route := options.RouteOption{Route: *config.RandomRoute.ToPtr()} opts = options.ClusterEchoOptions{ EchoOptions: &options.EchoOptions{ Message: "hello", @@ -269,20 +269,18 @@ func (suite *GlideTestSuite) TestEchoCluster() { RouteOption: &route, } response, err = client.EchoWithOptions(opts) - fmt.Println("response: ", response) assert.NoError(t, err) assert.True(t, response.IsSingleValue()) // default sections, multi node route - routeMultiNode := config.Route(*config.AllPrimaries.ToPtr()) + route = options.RouteOption{Route: *config.AllPrimaries.ToPtr()} opts = options.ClusterEchoOptions{ EchoOptions: &options.EchoOptions{ Message: "hello", }, - RouteOption: &routeMultiNode, + RouteOption: &route, } response, err = client.EchoWithOptions(opts) - fmt.Println("response: ", response) assert.NoError(t, err) assert.True(t, response.IsMultiValue()) for _, messages := range response.MultiValue() { From d19c94e2164bf65d306efa34c79a1644cbef36e1 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Fri, 31 Jan 2025 10:24:56 +0000 Subject: [PATCH 15/20] Fix merge conflict Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 20277eb705..1dd2724265 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -322,23 +322,13 @@ func (client *GlideClusterClient) TimeWithOptions(opts options.RouteOption) (Clu // // [valkey.io]: https://valkey.io/commands/echo/ func (client *GlideClusterClient) EchoWithOptions(echoOptions options.ClusterEchoOptions) (ClusterValue[string], error) { - if echoOptions.RouteOption.Route == nil { - response, err := client.executeCommand(C.Echo, echoOptions.ToArgs()) - if err != nil { - return createEmptyClusterValue[string](), err - } - data, err := handleStringResponse(response) - if err != nil { - return createEmptyClusterValue[string](), err - } - return createClusterSingleValue[string](data), nil - } response, err := client.executeCommandWithRoute(C.Echo, echoOptions.ToArgs(), echoOptions.RouteOption.Route) if err != nil { return createEmptyClusterValue[string](), err } - if (echoOptions.RouteOption.Route).IsMultiNode() { + if echoOptions.RouteOption.Route != nil && + (echoOptions.RouteOption.Route).IsMultiNode() { data, err := handleStringToStringMapResponse(response) if err != nil { return createEmptyClusterValue[string](), err From 181a1b2b697db1017eb31521fe368048d9d493bd Mon Sep 17 00:00:00 2001 From: EdricCua Date: Fri, 31 Jan 2025 22:48:08 +0000 Subject: [PATCH 16/20] Fix merge conflict Signed-off-by: EdricCua --- go/api/base_client.go | 141 +++++++++++++++++++ go/api/command_options.go | 22 +++ go/api/glide_cluster_client.go | 25 ++++ go/api/options/db_size_options.go | 16 +++ go/api/options/zmpop_options.go | 34 +++++ go/api/response_handlers.go | 44 ++++++ go/api/response_types.go | 21 +++ go/api/server_management_cluster_commands.go | 4 +- go/api/sorted_set_commands.go | 9 ++ go/integTest/cluster_commands_test.go | 11 ++ go/integTest/shared_commands_test.go | 81 +++++++++++ 11 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 go/api/options/db_size_options.go create mode 100644 go/api/options/zmpop_options.go diff --git a/go/api/base_client.go b/go/api/base_client.go index 8de1b86b49..12ad2489aa 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -4471,6 +4471,147 @@ func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[K return handleKeyWithMemberAndScoreResponse(result) } +// Blocks the connection until it pops and returns a member-score pair from the first non-empty sorted set, with the +// given keys being checked in the order they are provided. +// BZMPop is the blocking variant of [baseClient.ZMPop]. +// +// Note: +// - When in cluster mode, all keys must map to the same hash slot. +// - BZMPop is a client blocking command, see [Blocking Commands] for more details and best practices. +// +// Since: +// +// Valkey 7.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - An array of keys to lists. +// scoreFilter - The element pop criteria - either [api.MIN] or [api.MAX] to pop members with the lowest/highest +// scores accordingly. +// timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of `0` will block +// indefinitely. +// +// Return value: +// +// An object containing the following elements: +// - The key name of the set from which the element was popped. +// - An array of member scores of the popped elements. +// Returns `nil` if no member could be popped and the timeout expired. +// +// For example: +// +// result, err := client.ZAdd("my_list", map[string]float64{"five": 5.0, "six": 6.0}) +// result, err := client.BZMPop([]string{"my_list"}, api.MAX, float64(0.1)) +// result["my_list"] = []MemberAndScore{{Member: "six", Score: 6.0}} +// +// [valkey.io]: https://valkey.io/commands/bzmpop/ +// [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands +func (client *baseClient) BZMPop( + keys []string, + scoreFilter ScoreFilter, + timeoutSecs float64, +) (Result[KeyWithArrayOfMembersAndScores], error) { + scoreFilterStr, err := scoreFilter.toString() + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + // Check for potential length overflow. + if len(keys) > math.MaxInt-3 { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ + Msg: "Length overflow for the provided keys", + } + } + + // args slice will have 3 more arguments with the keys provided. + args := make([]string, 0, len(keys)+3) + args = append(args, utils.FloatToString(timeoutSecs), strconv.Itoa(len(keys))) + args = append(args, keys...) + args = append(args, scoreFilterStr) + result, err := client.executeCommand(C.BZMPop, args) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + return handleKeyWithArrayOfMembersAndScoresResponse(result) +} + +// Blocks the connection until it pops and returns a member-score pair from the first non-empty sorted set, with the +// given keys being checked in the order they are provided. +// BZMPop is the blocking variant of [baseClient.ZMPop]. +// +// Note: +// - When in cluster mode, all keys must map to the same hash slot. +// - BZMPop is a client blocking command, see [Blocking Commands] for more details and best practices. +// +// Since: +// +// Valkey 7.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// keys - An array of keys to lists. +// scoreFilter - The element pop criteria - either [api.MIN] or [api.MAX] to pop members with the lowest/highest +// scores accordingly. +// count - The maximum number of popped elements. +// timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. +// +// Return value: +// +// An object containing the following elements: +// - The key name of the set from which the element was popped. +// - An array of member scores of the popped elements. +// Returns `nil` if no member could be popped and the timeout expired. +// +// For example: +// +// result, err := client.ZAdd("my_list", map[string]float64{"five": 5.0, "six": 6.0}) +// result, err := client.BZMPopWithOptions([]string{"my_list"}, api.MAX, 0.1, options.NewZMPopOptions().SetCount(2)) +// result["my_list"] = []MemberAndScore{{Member: "six", Score: 6.0}, {Member: "five", Score 5.0}} +// +// [valkey.io]: https://valkey.io/commands/bzmpop/ +// [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands +func (client *baseClient) BZMPopWithOptions( + keys []string, + scoreFilter ScoreFilter, + timeoutSecs float64, + opts *options.ZMPopOptions, +) (Result[KeyWithArrayOfMembersAndScores], error) { + scoreFilterStr, err := scoreFilter.toString() + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + // Check for potential length overflow. + if len(keys) > math.MaxInt-5 { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ + Msg: "Length overflow for the provided keys", + } + } + + // args slice will have 5 more arguments with the keys provided. + args := make([]string, 0, len(keys)+5) + args = append(args, utils.FloatToString(timeoutSecs), strconv.Itoa(len(keys))) + args = append(args, keys...) + args = append(args, scoreFilterStr) + if opts != nil { + optionArgs, err := opts.ToArgs() + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + args = append(args, optionArgs...) + } + result, err := client.executeCommand(C.BZMPop, args) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + return handleKeyWithArrayOfMembersAndScoresResponse(result) +} + // Returns the specified range of elements in the sorted set stored at `key`. // `ZRANGE` can perform different types of range queries: by index (rank), by the score, or by lexicographical order. // diff --git a/go/api/command_options.go b/go/api/command_options.go index 23bc686e1c..af3260c11c 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -281,6 +281,28 @@ func (listDirection ListDirection) toString() (string, error) { } } +// Mandatory option for [ZMPop] and for [BZMPop]. +// Defines which elements to pop from the sorted set. +type ScoreFilter string + +const ( + // Pop elements with the highest scores. + MAX ScoreFilter = "MAX" + // Pop elements with the lowest scores. + MIN ScoreFilter = "MIN" +) + +func (scoreFilter ScoreFilter) toString() (string, error) { + switch scoreFilter { + case MAX: + return string(MAX), nil + case MIN: + return string(MIN), nil + default: + return "", &errors.RequestError{Msg: "Invalid score filter"} + } +} + // Optional arguments to Restore(key string, ttl int64, value string, option *RestoreOptions) // // Note IDLETIME and FREQ modifiers cannot be set at the same time. diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 1dd2724265..61ccf01c6f 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -299,6 +299,31 @@ func (client *GlideClusterClient) TimeWithOptions(opts options.RouteOption) (Clu return handleTimeClusterResponse(result) } +// Returns the number of keys in the database. +// +// Return value: +// +// The number of keys in the database. +// +// Example: +// +// route := api.SimpleNodeRoute(api.RandomRoute) +// options := options.NewDBOptionsBuilder().SetRoute(route) +// result, err := client.DBSizeWithOption(route) +// if err != nil { +// // handle error +// } +// fmt.Println(result) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/dbsize/ +func (client *GlideClusterClient) DBSizeWithOptions(opts options.RouteOption) (int64, error) { + result, err := client.executeCommandWithRoute(C.DBSize, []string{}, opts.Route) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + // Echo the provided message back. // The command will be routed a random node, unless `Route` in `echoOptions` is provided. // diff --git a/go/api/options/db_size_options.go b/go/api/options/db_size_options.go new file mode 100644 index 0000000000..7ebdb6a6de --- /dev/null +++ b/go/api/options/db_size_options.go @@ -0,0 +1,16 @@ +package options + +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + +type DBSizeOptions struct { + Route config.Route +} + +func NewTimeOptionsBuilder() *DBSizeOptions { + return &DBSizeOptions{} +} + +func (dbSizeOptions *DBSizeOptions) SetRoute(route config.Route) *DBSizeOptions { + dbSizeOptions.Route = route + return dbSizeOptions +} diff --git a/go/api/options/zmpop_options.go b/go/api/options/zmpop_options.go new file mode 100644 index 0000000000..5602f8f008 --- /dev/null +++ b/go/api/options/zmpop_options.go @@ -0,0 +1,34 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +// Optional arguments for `ZMPop` and `BZMPop` in [SortedSetCommands] +type ZMPopOptions struct { + count int64 + countIsSet bool +} + +func NewZMPopOptions() *ZMPopOptions { + return &ZMPopOptions{} +} + +// Set the count. +func (zmpo *ZMPopOptions) SetCount(count int64) *ZMPopOptions { + zmpo.count = count + zmpo.countIsSet = true + return zmpo +} + +func (zmpo *ZMPopOptions) ToArgs() ([]string, error) { + var args []string + + if zmpo.countIsSet { + args = append(args, "COUNT", utils.IntToString(zmpo.count)) + } + + return args, nil +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 4136dd09cb..0b923d3753 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -562,6 +562,50 @@ func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Re return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil } +func handleKeyWithArrayOfMembersAndScoresResponse( + response *C.struct_CommandResponse, +) (Result[KeyWithArrayOfMembersAndScores], error) { + defer C.free_command_response(response) + + if response.response_type == uint32(C.Null) { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), nil + } + + typeErr := checkResponseType(response, C.Array, true) + if typeErr != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), typeErr + } + + slice, err := parseArray(response) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + + arr := slice.([]interface{}) + key := arr[0].(string) + converted, err := mapConverter[float64]{ + nil, + false, + }.convert(arr[1]) + if err != nil { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), err + } + res, ok := converted.(map[string]float64) + + if !ok { + return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ + Msg: fmt.Sprintf("unexpected type of second element: %T", converted), + } + } + memberAndScoreArray := make([]MemberAndScore, 0, len(res)) + + for k, v := range res { + memberAndScoreArray = append(memberAndScoreArray, MemberAndScore{k, v}) + } + + return CreateKeyWithArrayOfMembersAndScoresResult(KeyWithArrayOfMembersAndScores{key, memberAndScoreArray}), nil +} + func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) diff --git a/go/api/response_types.go b/go/api/response_types.go index 1036c55c6b..aee9e2a3d4 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -23,6 +23,17 @@ type KeyWithMemberAndScore struct { Score float64 } +// Response of the [ZMPop] and [BZMPop] command. +type KeyWithArrayOfMembersAndScores struct { + Key string + MembersAndScores []MemberAndScore +} + +type MemberAndScore struct { + Member string + Score float64 +} + // Response type of [XAutoClaim] command. type XAutoClaimResponse struct { NextEntry string @@ -77,6 +88,16 @@ func CreateNilKeyWithMemberAndScoreResult() Result[KeyWithMemberAndScore] { return Result[KeyWithMemberAndScore]{val: KeyWithMemberAndScore{"", "", 0.0}, isNil: true} } +func CreateKeyWithArrayOfMembersAndScoresResult( + kmsVals KeyWithArrayOfMembersAndScores, +) Result[KeyWithArrayOfMembersAndScores] { + return Result[KeyWithArrayOfMembersAndScores]{val: kmsVals, isNil: false} +} + +func CreateNilKeyWithArrayOfMembersAndScoresResult() Result[KeyWithArrayOfMembersAndScores] { + return Result[KeyWithArrayOfMembersAndScores]{val: KeyWithArrayOfMembersAndScores{"", nil}, isNil: true} +} + // Enum to distinguish value types stored in `ClusterValue` type ValueType int diff --git a/go/api/server_management_cluster_commands.go b/go/api/server_management_cluster_commands.go index b49737c05b..87a04e2813 100644 --- a/go/api/server_management_cluster_commands.go +++ b/go/api/server_management_cluster_commands.go @@ -4,7 +4,7 @@ package api import "github.com/valkey-io/valkey-glide/go/glide/api/options" -// ServerManagementClusterCommands supports commands for the "Server Management Commands" group for cluster client. +// ServerManagementCommands supports commands for the "Server Management" group for a cluster client. // // See [valkey.io] for details. // @@ -15,4 +15,6 @@ type ServerManagementClusterCommands interface { InfoWithOptions(options ClusterInfoOptions) (ClusterValue[string], error) TimeWithOptions(routeOption options.RouteOption) (ClusterValue[[]string], error) + + DBSizeWithOptions(routeOption options.RouteOption) (int64, error) } diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 1aecdfeb1d..4babb17c55 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -36,6 +36,15 @@ type SortedSetCommands interface { BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) + BZMPop(keys []string, scoreFilter ScoreFilter, timeoutSecs float64) (Result[KeyWithArrayOfMembersAndScores], error) + + BZMPopWithOptions( + keys []string, + scoreFilter ScoreFilter, + timeoutSecs float64, + options *options.ZMPopOptions, + ) (Result[KeyWithArrayOfMembersAndScores], error) + ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[string]float64, error) diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index a6a7da5d3c..f995532d5d 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -245,6 +245,17 @@ func (suite *GlideTestSuite) TestTimeWithInvalidRoute() { assert.Empty(suite.T(), result.SingleValue()) } +func (suite *GlideTestSuite) TestDBSizeRandomRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.RandomRoute) + options := options.RouteOption{Route: route} + result, err := client.DBSizeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.NotEmpty(suite.T(), result) + assert.Greater(suite.T(), result, int64(0)) +} + func (suite *GlideTestSuite) TestEchoCluster() { client := suite.defaultClusterClient() t := suite.T() diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index e6cece6883..9104f844eb 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -2869,6 +2869,87 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { }) } +func (suite *GlideTestSuite) TestBZMPopAndBZMPopWithOptions() { + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1" + uuid.NewString() + key2 := "{key}-2" + uuid.NewString() + key3 := "{key}-3" + uuid.NewString() + + res1, err := client.BZMPop([]string{key1}, api.MIN, float64(0.1)) + assert.Nil(suite.T(), err) + assert.True(suite.T(), res1.IsNil()) + + membersScoreMap := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + + res3, err := client.ZAdd(key1, membersScoreMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res3) + res4, err := client.ZAdd(key2, membersScoreMap) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res4) + + // Try to pop the top 2 elements from key1 + res5, err := client.BZMPopWithOptions([]string{key1}, api.MAX, float64(0.1), options.NewZMPopOptions().SetCount(2)) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), key1, res5.Value().Key) + assert.ElementsMatch( + suite.T(), + []api.MemberAndScore{ + {Member: "three", Score: 3.0}, + {Member: "two", Score: 2.0}, + }, + res5.Value().MembersAndScores, + ) + + // Try to pop the minimum value from key2 + res6, err := client.BZMPop([]string{key2}, api.MIN, float64(0.1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + api.CreateKeyWithArrayOfMembersAndScoresResult( + api.KeyWithArrayOfMembersAndScores{ + Key: key2, + MembersAndScores: []api.MemberAndScore{ + {Member: "one", Score: 1.0}, + }, + }, + ), + res6, + ) + + // Pop the minimum value from multiple keys + res7, err := client.BZMPop([]string{key1, key2}, api.MIN, float64(0.1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + api.CreateKeyWithArrayOfMembersAndScoresResult( + api.KeyWithArrayOfMembersAndScores{ + Key: key1, + MembersAndScores: []api.MemberAndScore{ + {Member: "one", Score: 1.0}, + }, + }, + ), + res7, + ) + + suite.verifyOK(client.Set(key3, "value")) + + // Popping a non-existent value in key3 + res8, err := client.BZMPop([]string{key3}, api.MIN, float64(0.1)) + assert.True(suite.T(), res8.IsNil()) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &errors.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestLSet() { suite.runWithDefaultClients(func(client api.BaseClient) { key := uuid.NewString() From 046036a2c94626f6f905d76fbd75487fb89f7b0a Mon Sep 17 00:00:00 2001 From: EdricCua Date: Fri, 31 Jan 2025 22:54:07 +0000 Subject: [PATCH 17/20] Fix review comment Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index a55781f3b7..8228b22864 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -360,7 +360,7 @@ func (client *GlideClusterClient) DBSizeWithOptions(opts options.RouteOption) (i // // A map where each address is the key and its corresponding node response is the information for the default sections. // -// For example: +// Example: // // response, err := clusterClient.EchoWithOptions(opts) // if err != nil { From 1844b96daea521fb53b816675a9eaa2085e359d4 Mon Sep 17 00:00:00 2001 From: EdricCua Date: Fri, 31 Jan 2025 23:03:31 +0000 Subject: [PATCH 18/20] Fix review comment Signed-off-by: EdricCua --- go/api/glide_cluster_client.go | 25 ------------------------- go/api/response_types.go | 11 ----------- 2 files changed, 36 deletions(-) diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 8228b22864..d558584aa0 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -324,31 +324,6 @@ func (client *GlideClusterClient) DBSizeWithOptions(opts options.RouteOption) (i return handleIntResponse(result) } -// Returns the number of keys in the database. -// -// Return value: -// -// The number of keys in the database. -// -// Example: -// -// route := api.SimpleNodeRoute(api.RandomRoute) -// options := options.NewDBOptionsBuilder().SetRoute(route) -// result, err := client.DBSizeWithOption(route) -// if err != nil { -// // handle error -// } -// fmt.Println(result) // Output: 1 -// -// [valkey.io]: https://valkey.io/commands/dbsize/ -func (client *GlideClusterClient) DBSizeWithOptions(opts options.RouteOption) (int64, error) { - result, err := client.executeCommandWithRoute(C.DBSize, []string{}, opts.Route) - if err != nil { - return defaultIntResponse, err - } - return handleIntResponse(result) -} - // Echo the provided message back. // The command will be routed a random node, unless `Route` in `echoOptions` is provided. // diff --git a/go/api/response_types.go b/go/api/response_types.go index 510d653597..db548c402f 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -35,17 +35,6 @@ type MemberAndScore struct { Score float64 } -// Response of the [ZMPop] and [BZMPop] command. -type KeyWithArrayOfMembersAndScores struct { - Key string - MembersAndScores []MemberAndScore -} - -type MemberAndScore struct { - Member string - Score float64 -} - // Response type of [XAutoClaim] command. type XAutoClaimResponse struct { NextEntry string From 86cc1856a089a618e877d7a065a454c70504df9e Mon Sep 17 00:00:00 2001 From: EdricCua Date: Fri, 31 Jan 2025 23:12:17 +0000 Subject: [PATCH 19/20] Fix review comment Signed-off-by: EdricCua --- go/api/response_handlers.go | 44 ------------------------------------- 1 file changed, 44 deletions(-) diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 9ac7cbf610..8bdd51f7c2 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -654,50 +654,6 @@ func handleMemberAndScoreArrayResponse(response *C.struct_CommandResponse) ([]Me return result, nil } -func handleKeyWithArrayOfMembersAndScoresResponse( - response *C.struct_CommandResponse, -) (Result[KeyWithArrayOfMembersAndScores], error) { - defer C.free_command_response(response) - - if response.response_type == uint32(C.Null) { - return CreateNilKeyWithArrayOfMembersAndScoresResult(), nil - } - - typeErr := checkResponseType(response, C.Array, true) - if typeErr != nil { - return CreateNilKeyWithArrayOfMembersAndScoresResult(), typeErr - } - - slice, err := parseArray(response) - if err != nil { - return CreateNilKeyWithArrayOfMembersAndScoresResult(), err - } - - arr := slice.([]interface{}) - key := arr[0].(string) - converted, err := mapConverter[float64]{ - nil, - false, - }.convert(arr[1]) - if err != nil { - return CreateNilKeyWithArrayOfMembersAndScoresResult(), err - } - res, ok := converted.(map[string]float64) - - if !ok { - return CreateNilKeyWithArrayOfMembersAndScoresResult(), &errors.RequestError{ - Msg: fmt.Sprintf("unexpected type of second element: %T", converted), - } - } - memberAndScoreArray := make([]MemberAndScore, 0, len(res)) - - for k, v := range res { - memberAndScoreArray = append(memberAndScoreArray, MemberAndScore{k, v}) - } - - return CreateKeyWithArrayOfMembersAndScoresResult(KeyWithArrayOfMembersAndScores{key, memberAndScoreArray}), nil -} - func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) From 4675569211c5833fea86907004e395b741c670dd Mon Sep 17 00:00:00 2001 From: EdricCua Date: Fri, 31 Jan 2025 23:18:17 +0000 Subject: [PATCH 20/20] Fix review comment Signed-off-by: EdricCua --- go/integTest/cluster_commands_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index ca6e2e0872..f995532d5d 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -256,17 +256,6 @@ func (suite *GlideTestSuite) TestDBSizeRandomRoute() { assert.Greater(suite.T(), result, int64(0)) } -func (suite *GlideTestSuite) TestDBSizeRandomRoute() { - client := suite.defaultClusterClient() - route := config.Route(config.RandomRoute) - options := options.RouteOption{Route: route} - result, err := client.DBSizeWithOptions(options) - assert.NoError(suite.T(), err) - assert.NotNil(suite.T(), result) - assert.NotEmpty(suite.T(), result) - assert.Greater(suite.T(), result, int64(0)) -} - func (suite *GlideTestSuite) TestEchoCluster() { client := suite.defaultClusterClient() t := suite.T()