diff --git a/.github/workflows/scale-shr-test.yml b/.github/workflows/scale-shr-test.yml index f242fcba2a..f370e18b5d 100644 --- a/.github/workflows/scale-shr-test.yml +++ b/.github/workflows/scale-shr-test.yml @@ -4,7 +4,7 @@ on: jobs: hello-world: - runs-on: [self-hosted, linux, ARM64] + runs-on: [self-hosted, linux, ARM64, ephemeral] steps: - name: print Hello World run: echo "Hello World" diff --git a/go/api/base_client.go b/go/api/base_client.go index dd8c419710..8de1b86b49 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -6849,3 +6849,24 @@ func (client *baseClient) BitFieldRO(key string, commands []options.BitFieldROCo } return handleIntOrNilArrayResponse(result) } + +// Returns the server time. +// +// Return value: +// The current server time as a String array with two elements: +// A UNIX TIME and the amount of microseconds already elapsed in the current second. +// The returned array is in a [UNIX TIME, Microseconds already elapsed] format. +// +// For example: +// +// result, err := client.Time() +// result: [{1737051660} {994688}] +// +// [valkey.io]: https://valkey.io/commands/time/ +func (client *baseClient) Time() ([]string, error) { + result, err := client.executeCommand(C.Time, []string{}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 191acbe950..9c72a86562 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -267,6 +267,38 @@ func (client *GlideClusterClient) PingWithOptions(pingOptions options.ClusterPin return handleStringResponse(response) } +// Returns the server time. +// The command will be routed to a random node, unless Route in opts is provided. +// +// See [valkey.io] for details. +// +// Parameters: +// +// options - The TimeOptions type. +// +// Return value: +// +// The current server time as a String array with two elements: A UNIX TIME and the amount +// of microseconds already elapsed in the current second. +// The returned array is in a [UNIX TIME, Microseconds already elapsed] format. +// +// Example: +// +// route := config.Route(config.RandomRoute) +// opts := options.ClusterTimeOptions{ +// Route: &route, +// } +// fmt.Println(clusterResponse.SingleValue()) // Output: [1737994354 547816] +// +// [valkey.io]: https://valkey.io/commands/time/ +func (client *GlideClusterClient) TimeWithOptions(opts options.RouteOption) (ClusterValue[[]string], error) { + result, err := client.executeCommandWithRoute(C.Time, []string{}, opts.Route) + if err != nil { + return createEmptyClusterValue[[]string](), err + } + return handleTimeClusterResponse(result) +} + // Returns the number of keys in the database. // // Return value: diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 72f849063d..4136dd09cb 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -1003,3 +1003,53 @@ func handleXPendingDetailResponse(response *C.struct_CommandResponse) ([]XPendin return pendingDetails, nil } + +func handleRawStringArrayMapResponse(response *C.struct_CommandResponse) (map[string][]string, error) { + defer C.free_command_response(response) + typeErr := checkResponseType(response, C.Map, false) + if typeErr != nil { + return nil, typeErr + } + + data, err := parseMap(response) + if err != nil { + return nil, err + } + + result, err := mapConverter[[]string]{ + next: arrayConverter[string]{}, + canBeNil: false, + }.convert(data) + if err != nil { + return nil, err + } + mapResult, ok := result.(map[string][]string) + if !ok { + return nil, &errors.RequestError{Msg: "Unexpected conversion result type"} + } + + return mapResult, nil +} + +func handleTimeClusterResponse(response *C.struct_CommandResponse) (ClusterValue[[]string], error) { + // Handle multi-node response + if err := checkResponseType(response, C.Map, true); err == nil { + mapData, err := handleRawStringArrayMapResponse(response) + if err != nil { + return createEmptyClusterValue[[]string](), err + } + multiNodeTimes := make(map[string][]string) + for nodeName, nodeTimes := range mapData { + multiNodeTimes[nodeName] = nodeTimes + } + + return createClusterMultiValue(multiNodeTimes), nil + } + + // Handle single node response + data, err := handleStringArrayResponse(response) + if err != nil { + return createEmptyClusterValue[[]string](), err + } + return createClusterSingleValue(data), nil +} diff --git a/go/api/server_management_cluster_commands.go b/go/api/server_management_cluster_commands.go index 73eead96b2..87a04e2813 100644 --- a/go/api/server_management_cluster_commands.go +++ b/go/api/server_management_cluster_commands.go @@ -14,5 +14,7 @@ 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/server_management_commands.go b/go/api/server_management_commands.go index d6909c72a9..48ded7974f 100644 --- a/go/api/server_management_commands.go +++ b/go/api/server_management_commands.go @@ -19,4 +19,6 @@ type ServerManagementCommands interface { InfoWithOptions(options InfoOptions) (string, error) DBSize() (int64, error) + + Time() ([]string, error) } diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index e0209f5b43..19a8167dce 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -187,6 +187,64 @@ func (suite *GlideTestSuite) TestPingWithOptions_InvalidRoute() { assert.Empty(suite.T(), result) } +func (suite *GlideTestSuite) TestTimeWithoutRoute() { + client := suite.defaultClusterClient() + options := options.RouteOption{Route: nil} + result, err := client.TimeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.False(suite.T(), result.IsEmpty()) + assert.True(suite.T(), result.IsSingleValue()) + assert.NotEmpty(suite.T(), result.SingleValue()) + assert.IsType(suite.T(), "", result.SingleValue()[0]) + assert.Equal(suite.T(), 2, len(result.SingleValue())) +} + +func (suite *GlideTestSuite) TestTimeWithAllNodesRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.AllNodes) + options := options.RouteOption{Route: route} + result, err := client.TimeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.False(suite.T(), result.IsEmpty()) + assert.True(suite.T(), result.IsMultiValue()) + + multiValue := result.MultiValue() + assert.Greater(suite.T(), len(multiValue), 1) + + for nodeName, timeStrings := range multiValue { + assert.NotEmpty(suite.T(), timeStrings, "Node %s should have time values", nodeName) + for _, timeStr := range timeStrings { + assert.IsType(suite.T(), "", timeStr) + } + } +} + +func (suite *GlideTestSuite) TestTimeWithRandomRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.RandomRoute) + options := options.RouteOption{Route: route} + result, err := client.TimeWithOptions(options) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.False(suite.T(), result.IsEmpty()) + assert.True(suite.T(), result.IsSingleValue()) + assert.NotEmpty(suite.T(), result.SingleValue()) + assert.IsType(suite.T(), "", result.SingleValue()[0]) + assert.Equal(suite.T(), 2, len(result.SingleValue())) +} + +func (suite *GlideTestSuite) TestTimeWithInvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) + options := options.RouteOption{Route: invalidRoute} + result, err := client.TimeWithOptions(options) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), result.IsEmpty()) + assert.Empty(suite.T(), result.SingleValue()) +} + func (suite *GlideTestSuite) TestDBSizeRandomRoute() { client := suite.defaultClusterClient() route := config.Route(config.RandomRoute) diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 4bede26248..792627422d 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -4,7 +4,9 @@ package integTest import ( "fmt" + "strconv" "strings" + "time" "github.com/google/uuid" "github.com/valkey-io/valkey-glide/go/glide/api" @@ -483,3 +485,34 @@ func (suite *GlideTestSuite) TestPingWithOptions_ClosedClient() { assert.Equal(suite.T(), "", result) assert.IsType(suite.T(), &errors.ClosingError{}, err) } + +func (suite *GlideTestSuite) TestTime_Success() { + client := suite.defaultClient() + results, err := client.Time() + + assert.Nil(suite.T(), err) + assert.Len(suite.T(), results, 2) + + now := time.Now().Unix() - 1 + + timestamp, err := strconv.ParseInt(results[0], 10, 64) + assert.Nil(suite.T(), err) + assert.Greater(suite.T(), timestamp, now) + + microseconds, err := strconv.ParseInt(results[1], 10, 64) + assert.Nil(suite.T(), err) + assert.Less(suite.T(), microseconds, int64(1000000)) +} + +func (suite *GlideTestSuite) TestTime_Error() { + client := suite.defaultClient() + + // Disconnect the client or simulate an error condition + client.Close() + + results, err := client.Time() + + assert.NotNil(suite.T(), err) + assert.Nil(suite.T(), results) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +}