From 3f92b700b5766881ce621c0012ca6f05fba982a5 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 21 Jan 2025 18:14:20 -0800 Subject: [PATCH 1/5] Go: `INFO`. Signed-off-by: Yury-Fridlyand --- go/api/command_options.go | 67 ++++++++++++++++ go/api/glide_client.go | 56 +++++++++++++- go/api/glide_cluster_client.go | 97 +++++++++++++++++++++++- go/api/request_routing_config.go | 22 ++++++ go/api/response_types.go | 47 +++++++----- go/api/server_management_commands.go | 6 +- go/integTest/cluster_commands_test.go | 82 +++++++++++++++++++- go/integTest/glide_test_suite_test.go | 18 +++-- go/integTest/json_module_test.go | 10 ++- go/integTest/shared_commands_test.go | 8 +- go/integTest/standalone_commands_test.go | 37 +++++++++ go/integTest/vss_module_test.go | 10 ++- 12 files changed, 412 insertions(+), 48 deletions(-) diff --git a/go/api/command_options.go b/go/api/command_options.go index dcf17446bc..c35a7a2ef4 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -355,3 +355,70 @@ func (opts *RestoreOptions) toArgs() ([]string, error) { } return args, err } + +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 `Info` for standalone client +type InfoOptions 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 +} + +// Optional arguments for `Info` for cluster client +type ClusterInfoOptions struct { + *InfoOptions + // 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 *route +} + +func (opts *InfoOptions) 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_client.go b/go/api/glide_client.go index 51ea9ef4b4..b9b2b7a8bf 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -46,7 +46,7 @@ func (client *glideClient) CustomCommand(args []string) (interface{}, error) { func (client *glideClient) ConfigSet(parameters map[string]string) (string, error) { result, err := client.executeCommand(C.ConfigSet, utils.MapToString(parameters)) if err != nil { - return "", err + return defaultStringResponse, err } return handleStringResponse(result) } @@ -62,7 +62,59 @@ func (client *glideClient) ConfigGet(args []string) (map[string]string, error) { func (client *glideClient) Select(index int64) (string, error) { result, err := client.executeCommand(C.Select, []string{utils.IntToString(index)}) if err != nil { - return "", err + return defaultStringResponse, err + } + + return handleStringResponse(result) +} + +// Gets information and statistics about the server. +// +// See [valkey.io] for details. +// +// Return value: +// +// A string with the information for the default sections. +// +// Example: +// +// response, err := standaloneClient.Info(opts) +// if err != nil { +// // handle error +// } +// fmt.Println(response) +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *glideClient) Info() (string, error) { + return client.InfoWithOptions(InfoOptions{[]Section{}}) +} + +// Gets information and statistics about the server. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - Additional command parameters, see [InfoOptions] for more details. +// +// Return value: +// +// A string containing the information for the sections requested. +// +// Example: +// +// opts := api.InfoOptions{Sections: []api.Section{api.Server}} +// response, err := standaloneClient.InfoWithOptions(opts) +// if err != nil { +// // handle error +// } +// fmt.Println(response) +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *glideClient) InfoWithOptions(options InfoOptions) (string, error) { + result, err := client.executeCommand(C.Info, options.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 cc672a91b5..f69e3f790a 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -13,6 +13,7 @@ var _ GlideClusterClient = (*glideClusterClient)(nil) type GlideClusterClient interface { BaseClient GenericClusterCommands + ServerManagementClusterCommands } // glideClusterClient implements cluster mode operations by extending baseClient functionality. @@ -33,11 +34,101 @@ func NewGlideClusterClient(config *GlideClusterClientConfiguration) (GlideCluste func (client *glideClusterClient) CustomCommand(args []string) (ClusterValue[interface{}], error) { res, err := client.executeCommand(C.CustomCommand, args) if err != nil { - return CreateEmptyClusterValue(), err + return createEmptyClusterValue[interface{}](), err } data, err := handleInterfaceResponse(res) if err != nil { - return CreateEmptyClusterValue(), err + return createEmptyClusterValue[interface{}](), err } - return CreateClusterValue(data), nil + return createClusterValue[interface{}](data), nil +} + +// Gets information and statistics about the server. +// +// The command will be routed to all primary nodes. +// +// See [valkey.io] for details. +// +// Return value: +// +// A map where each address is the key and its corresponding node response is the information for the default sections. +// +// Example: +// +// response, err := clusterClient.Info(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/info/ +func (client *glideClusterClient) Info() (map[string]string, error) { + result, err := client.executeCommand(C.Info, []string{}) + if err != nil { + return nil, err + } + + return handleStringToStringMapResponse(result) +} + +// Gets information and statistics about the server. +// +// The command will be routed to all primary nodes, unless `route` in [ClusterInfoOptions] is provided. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - Additional command parameters, see [ClusterInfoOptions] for more details. +// +// Return value: +// +// When specifying a route other than a single node or when route is not given, +// it returns a map where each address is the key and its corresponding node response is the value. +// When a single node route is given, command returns a string containing the information for the sections requested. +// +// Example: +// +// opts := api.ClusterInfoOptions{ +// InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, +// Route: api.RandomRoute.ToPtr(), +// } +// response, err := clusterClient.InfoWithOptions(opts) +// if err != nil { +// // handle error +// } +// // Command sent to a single random node via RANDOM route, expecting SingleValue result as a `string`. +// fmt.Println(response.SingleValue()) +// +// [valkey.io]: https://valkey.io/commands/info/ +func (client *glideClusterClient) InfoWithOptions(options ClusterInfoOptions) (ClusterValue[string], error) { + if options.Route == nil { + response, err := client.executeCommand(C.Info, 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 + } + response, err := client.executeCommandWithRoute(C.Info, options.toArgs(), *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) + if err != nil { + return createEmptyClusterValue[string](), err + } + return createClusterSingleValue[string](data), nil } diff --git a/go/api/request_routing_config.go b/go/api/request_routing_config.go index 3a1c53b124..027a581c9b 100644 --- a/go/api/request_routing_config.go +++ b/go/api/request_routing_config.go @@ -17,6 +17,7 @@ import ( // - [api.ByAddressRoute] type route interface { toRoutesProtobuf() (*protobuf.Routes, error) + isMultiNode() bool } type SimpleNodeRoute int @@ -46,6 +47,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: @@ -111,6 +121,10 @@ func (slotIdRoute *SlotIdRoute) toRoutesProtobuf() (*protobuf.Routes, error) { return request, nil } +func (route *SlotIdRoute) isMultiNode() bool { + return false +} + // Request routing configuration overrides the [api.ReadFrom] connection configuration. // If SlotTypeReplica is used, the request will be routed to a replica, even if the strategy is ReadFrom [api.PreferReplica]. type SlotKeyRoute struct { @@ -141,6 +155,10 @@ func (slotKeyRoute *SlotKeyRoute) toRoutesProtobuf() (*protobuf.Routes, error) { return request, nil } +func (route *SlotKeyRoute) isMultiNode() bool { + return false +} + // Routes a request to a node by its address. type ByAddressRoute struct { host string @@ -189,3 +207,7 @@ func (byAddressRoute *ByAddressRoute) toRoutesProtobuf() (*protobuf.Routes, erro } return request, nil } + +func (route *ByAddressRoute) isMultiNode() bool { + return false +} 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/api/server_management_commands.go b/go/api/server_management_commands.go index 3653f17903..1b70bbe553 100644 --- a/go/api/server_management_commands.go +++ b/go/api/server_management_commands.go @@ -2,7 +2,7 @@ package api -// ServerManagementCommands supports commands and transactions for the "Server Management" group for a standalone client. +// ServerManagementCommands supports commands for the "Server Management" group for a standalone client. // // See [valkey.io] for details. // @@ -61,4 +61,8 @@ type ServerManagementCommands interface { // // [valkey.io]: https://valkey.io/commands/config-set/ ConfigSet(parameters map[string]string) (string, error) + + Info() (string, error) + + InfoWithOptions(options InfoOptions) (string, error) } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 142f0cf273..1aafad40b8 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" ) func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { @@ -14,7 +15,7 @@ func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { assert.Nil(suite.T(), err) // INFO is routed to all primary nodes by default - for _, value := range result.Value().(map[string]interface{}) { + for _, value := range result.MultiValue() { assert.True(suite.T(), strings.Contains(value.(string), "# Stats")) } } @@ -25,5 +26,82 @@ func (suite *GlideTestSuite) TestClusterCustomCommandEcho() { assert.Nil(suite.T(), err) // ECHO is routed to a single random node - assert.Equal(suite.T(), "GO GLIDE GO", result.Value().(string)) + assert.Equal(suite.T(), "GO GLIDE GO", result.SingleValue().(string)) +} + +func (suite *GlideTestSuite) TestInfoCluster() { + DEFAULT_INFO_SECTIONS := []string{ + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Errorstats", + "Cluster", + "Keyspace", + } + + client := suite.defaultClusterClient() + t := suite.T() + + // info without options + data, err := client.Info() + assert.NoError(t, err) + for _, info := range data { + for _, section := range DEFAULT_INFO_SECTIONS { + assert.Contains(t, info, "# "+section, "Section "+section+" is missing") + } + } + + // 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.ClusterInfoOptions{ + InfoOptions: &api.InfoOptions{Sections: sections}, + Route: nil, + } + response, err := client.InfoWithOptions(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.ClusterInfoOptions{ + InfoOptions: &api.InfoOptions{Sections: sections}, + Route: api.RandomRoute.ToPtr(), + } + response, err = client.InfoWithOptions(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.ClusterInfoOptions{ + InfoOptions: nil, + Route: api.AllPrimaries.ToPtr(), + } + response, err = client.InfoWithOptions(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") + } + } } diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index 9d4fed4fdf..f0434166ef 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -149,9 +149,8 @@ func getServerVersion(suite *GlideTestSuite) string { client, err := api.NewGlideClient(config) if err == nil && client != nil { defer client.Close() - // TODO use info command - info, _ := client.CustomCommand([]string{"info", "server"}) - return extractServerVersion(suite, info.(string)) + info, _ := client.InfoWithOptions(api.InfoOptions{Sections: []api.Section{api.Server}}) + return extractServerVersion(suite, info) } } if len(suite.clusterHosts) == 0 { @@ -169,11 +168,14 @@ func getServerVersion(suite *GlideTestSuite) string { client, err := api.NewGlideClusterClient(config) if err == nil && client != nil { defer client.Close() - // TODO use info command with route - info, _ := client.CustomCommand([]string{"info", "server"}) - for _, value := range info.Value().(map[string]interface{}) { - return extractServerVersion(suite, value.(string)) - } + + info, _ := client.InfoWithOptions( + api.ClusterInfoOptions{ + InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, + Route: api.RandomRoute.ToPtr(), + }, + ) + return extractServerVersion(suite, info.SingleValue()) } suite.T().Fatalf("Can't connect to any server to get version: %s", err.Error()) return "" diff --git a/go/integTest/json_module_test.go b/go/integTest/json_module_test.go index 8f0f33efc4..89868777c1 100644 --- a/go/integTest/json_module_test.go +++ b/go/integTest/json_module_test.go @@ -6,15 +6,17 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" ) func (suite *GlideTestSuite) TestModuleVerifyJsonLoaded() { client := suite.defaultClusterClient() - // TODO use INFO command - result, err := client.CustomCommand([]string{"INFO", "MODULES"}) + result, err := client.InfoWithOptions( + api.ClusterInfoOptions{InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, Route: nil}, + ) assert.Nil(suite.T(), err) - for _, value := range result.Value().(map[string]interface{}) { - assert.True(suite.T(), strings.Contains(value.(string), "# json_core_metrics")) + for _, value := range result.MultiValue() { + assert.True(suite.T(), strings.Contains(value, "# json_core_metrics")) } } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index e92c5e8df8..856ef30469 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -5658,17 +5658,17 @@ func (suite *GlideTestSuite) TestXPending() { resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "OK", resp.Value().(string)) + assert.Equal(suite.T(), "OK", resp.SingleValue().(string)) command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err = client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.True(suite.T(), resp.Value().(bool)) + assert.True(suite.T(), resp.SingleValue().(bool)) command = []string{"XGroup", "CreateConsumer", key, groupName, consumer2} resp, err = client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.True(suite.T(), resp.Value().(bool)) + assert.True(suite.T(), resp.SingleValue().(bool)) streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) assert.NoError(suite.T(), err) @@ -5913,7 +5913,7 @@ func (suite *GlideTestSuite) TestXPendingFailures() { command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.True(suite.T(), resp.Value().(bool)) + assert.True(suite.T(), resp.SingleValue().(bool)) _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) assert.NoError(suite.T(), err) diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 2d4a0ec31c..5dae164121 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -384,3 +384,40 @@ func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_SuccessfulSortByWeightA assert.Equal(suite.T(), "item1", sortResult[3].Value()) assert.Equal(suite.T(), "item3", sortResult[5].Value()) } + +func (suite *GlideTestSuite) TestInfoStandalone() { + DEFAULT_INFO_SECTIONS := []string{ + "Server", + "Clients", + "Memory", + "Persistence", + "Stats", + "Replication", + "CPU", + "Modules", + "Errorstats", + "Cluster", + "Keyspace", + } + + client := suite.defaultClient() + t := suite.T() + + // info without options + info, err := client.Info() + assert.NoError(t, err) + for _, section := range DEFAULT_INFO_SECTIONS { + assert.Contains(t, info, "# "+section, "Section "+section+" is missing") + } + + // info with option or with multiple options + sections := []api.Section{api.Cpu} + if suite.serverVersion >= "7.0.0" { + sections = append(sections, api.Memory) + } + info, err = client.InfoWithOptions(api.InfoOptions{Sections: sections}) + assert.NoError(t, err) + for _, section := range sections { + assert.Contains(t, strings.ToLower(info), strings.ToLower("# "+string(section)), "Section "+section+" is missing") + } +} diff --git a/go/integTest/vss_module_test.go b/go/integTest/vss_module_test.go index 1ebad87e13..94e42833d8 100644 --- a/go/integTest/vss_module_test.go +++ b/go/integTest/vss_module_test.go @@ -6,15 +6,17 @@ import ( "strings" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/api" ) func (suite *GlideTestSuite) TestModuleVerifyVssLoaded() { client := suite.defaultClusterClient() - // TODO use INFO command - result, err := client.CustomCommand([]string{"INFO", "MODULES"}) + result, err := client.InfoWithOptions( + api.ClusterInfoOptions{InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, Route: nil}, + ) assert.Nil(suite.T(), err) - for _, value := range result.Value().(map[string]interface{}) { - assert.True(suite.T(), strings.Contains(value.(string), "# search_index_stats")) + for _, value := range result.MultiValue() { + assert.True(suite.T(), strings.Contains(value, "# search_index_stats")) } } From eff425736388eda2880830f76c2ac34cc3f1155a Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 21 Jan 2025 18:22:44 -0800 Subject: [PATCH 2/5] add missing file Signed-off-by: Yury-Fridlyand --- go/api/server_management_cluster_commands.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 go/api/server_management_cluster_commands.go diff --git a/go/api/server_management_cluster_commands.go b/go/api/server_management_cluster_commands.go new file mode 100644 index 0000000000..c5922c9498 --- /dev/null +++ b/go/api/server_management_cluster_commands.go @@ -0,0 +1,14 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +// ServerManagementCommands supports commands for the "Server Management" group for a cluster client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#server +type ServerManagementClusterCommands interface { + Info() (map[string]string, error) + + InfoWithOptions(options ClusterInfoOptions) (ClusterValue[string], error) +} From 8b78ba0b1855c4e6cbbe3ff26026b9d50b9249e9 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Sat, 25 Jan 2025 11:46:38 -0800 Subject: [PATCH 3/5] fixes Signed-off-by: Yury-Fridlyand --- go/api/config/request_routing_config.go | 17 ++++++----------- go/integTest/cluster_commands_test.go | 5 +++-- go/integTest/glide_test_suite_test.go | 11 ++++++----- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/go/api/config/request_routing_config.go b/go/api/config/request_routing_config.go index 154fcc8ee3..47c3d7877a 100644 --- a/go/api/config/request_routing_config.go +++ b/go/api/config/request_routing_config.go @@ -21,6 +21,9 @@ type Route interface { IsMultiNode() bool } +type notMultiNode struct{} +func (_ *notMultiNode) IsMultiNode() bool { return false } + type SimpleNodeRoute int const ( @@ -96,6 +99,7 @@ func mapSlotType(slotType SlotType) (protobuf.SlotTypes, error) { type SlotIdRoute struct { slotType SlotType slotID int32 + notMultiNode } // - slotType: Defines type of the node being addressed. @@ -122,15 +126,12 @@ func (slotIdRoute *SlotIdRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { return request, nil } -func (route *SlotIdRoute) IsMultiNode() bool { - return false -} - // Request routing configuration overrides the [api.ReadFrom] connection configuration. // If SlotTypeReplica is used, the request will be routed to a replica, even if the strategy is ReadFrom [api.PreferReplica]. type SlotKeyRoute struct { slotType SlotType slotKey string + notMultiNode } // - slotType: Defines type of the node being addressed. @@ -156,14 +157,12 @@ func (slotKeyRoute *SlotKeyRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { return request, nil } -func (route *SlotKeyRoute) IsMultiNode() bool { - return false -} // Routes a request to a node by its address. type ByAddressRoute struct { host string port int32 + notMultiNode } // Create a route using hostname/address and port. @@ -208,7 +207,3 @@ func (byAddressRoute *ByAddressRoute) ToRoutesProtobuf() (*protobuf.Routes, erro } return request, nil } - -func (route *ByAddressRoute) IsMultiNode() bool { - return false -} diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 1aafad40b8..50693b5b17 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -7,6 +7,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" ) func (suite *GlideTestSuite) TestClusterCustomCommandInfo() { @@ -77,7 +78,7 @@ func (suite *GlideTestSuite) TestInfoCluster() { // same sections with random route opts = api.ClusterInfoOptions{ InfoOptions: &api.InfoOptions{Sections: sections}, - Route: api.RandomRoute.ToPtr(), + Route: config.RandomRoute.ToPtr(), } response, err = client.InfoWithOptions(opts) assert.NoError(t, err) @@ -94,7 +95,7 @@ func (suite *GlideTestSuite) TestInfoCluster() { // default sections, multi node route opts = api.ClusterInfoOptions{ InfoOptions: nil, - Route: api.AllPrimaries.ToPtr(), + Route: config.AllPrimaries.ToPtr(), } response, err = client.InfoWithOptions(opts) assert.NoError(t, err) diff --git a/go/integTest/glide_test_suite_test.go b/go/integTest/glide_test_suite_test.go index 2676926591..b6dc71032f 100644 --- a/go/integTest/glide_test_suite_test.go +++ b/go/integTest/glide_test_suite_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/config" ) type GlideTestSuite struct { @@ -141,12 +142,12 @@ func runClusterManager(suite *GlideTestSuite, args []string, ignoreExitCode bool func getServerVersion(suite *GlideTestSuite) string { var err error = nil if len(suite.standaloneHosts) > 0 { - config := api.NewGlideClientConfiguration(). + clientConfig := api.NewGlideClientConfiguration(). WithAddress(&suite.standaloneHosts[0]). WithUseTLS(suite.tls). WithRequestTimeout(5000) - client, err := api.NewGlideClient(config) + client, err := api.NewGlideClient(clientConfig) if err == nil && client != nil { defer client.Close() info, _ := client.InfoWithOptions(api.InfoOptions{Sections: []api.Section{api.Server}}) @@ -160,19 +161,19 @@ func getServerVersion(suite *GlideTestSuite) string { suite.T().Fatal("No server hosts configured") } - config := api.NewGlideClusterClientConfiguration(). + clientConfig := api.NewGlideClusterClientConfiguration(). WithAddress(&suite.clusterHosts[0]). WithUseTLS(suite.tls). WithRequestTimeout(5000) - client, err := api.NewGlideClusterClient(config) + client, err := api.NewGlideClusterClient(clientConfig) if err == nil && client != nil { defer client.Close() info, _ := client.InfoWithOptions( api.ClusterInfoOptions{ InfoOptions: &api.InfoOptions{Sections: []api.Section{api.Server}}, - Route: api.RandomRoute.ToPtr(), + Route: config.RandomRoute.ToPtr(), }, ) return extractServerVersion(suite, info.SingleValue()) From 4dde077317c28a472238601b450b0f3d60eb5d2b Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Sat, 25 Jan 2025 12:05:41 -0800 Subject: [PATCH 4/5] lint Signed-off-by: Yury-Fridlyand --- go/api/config/request_routing_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/api/config/request_routing_config.go b/go/api/config/request_routing_config.go index 47c3d7877a..8bcb0221ab 100644 --- a/go/api/config/request_routing_config.go +++ b/go/api/config/request_routing_config.go @@ -22,7 +22,8 @@ type Route interface { } type notMultiNode struct{} -func (_ *notMultiNode) IsMultiNode() bool { return false } + +func (*notMultiNode) IsMultiNode() bool { return false } type SimpleNodeRoute int @@ -157,7 +158,6 @@ func (slotKeyRoute *SlotKeyRoute) ToRoutesProtobuf() (*protobuf.Routes, error) { return request, nil } - // Routes a request to a node by its address. type ByAddressRoute struct { host string From 67b7e90089e7a4bed7b35e60b9c0e3459cf5d340 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Mon, 27 Jan 2025 14:46:34 -0800 Subject: [PATCH 5/5] fix Signed-off-by: Yury-Fridlyand --- go/api/response_types.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/go/api/response_types.go b/go/api/response_types.go index 2f983d769f..1036c55c6b 100644 --- a/go/api/response_types.go +++ b/go/api/response_types.go @@ -83,6 +83,7 @@ type ValueType int const ( SingleValue ValueType = 1 MultiValue ValueType = 2 + NoValue ValueType = 3 ) // Enum-like structure which stores either a single-node response or multi-node response. @@ -107,18 +108,19 @@ const ( // response := value.SingleValue() // // `response` stores the command output from a cluster node type ClusterValue[T any] struct { - valueType ValueType - value Result[any] + valueType ValueType + singleValue T + mutiValue map[string]T } // Get the single value stored (value returned by a single cluster node). func (value ClusterValue[T]) SingleValue() T { - return value.value.Value().(T) + return value.singleValue } // 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) + return value.mutiValue } // Get the value type @@ -135,7 +137,7 @@ func (value ClusterValue[T]) IsMultiValue() bool { } func (value ClusterValue[T]) IsEmpty() bool { - return value.value.IsNil() + return value.valueType == NoValue } func createClusterValue[T any](data any) ClusterValue[T] { @@ -149,22 +151,21 @@ func createClusterValue[T any](data any) ClusterValue[T] { func createClusterSingleValue[T any](data T) ClusterValue[T] { return ClusterValue[T]{ - valueType: SingleValue, - value: Result[any]{val: data, isNil: false}, + valueType: SingleValue, + singleValue: data, } } func createClusterMultiValue[T any](data map[string]T) ClusterValue[T] { return ClusterValue[T]{ valueType: MultiValue, - value: Result[any]{val: data, isNil: false}, + mutiValue: data, } } func createEmptyClusterValue[T any]() ClusterValue[T] { - var empty T return ClusterValue[T]{ - value: Result[any]{val: empty, isNil: true}, + valueType: NoValue, } }