Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[internal review] Go: implement standalone custom command #152

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions go/api/base_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ package api
// #cgo LDFLAGS: -L../target/release -lglide_rs
// #include "../lib.h"
//
// void successCallback(uintptr_t channelPtr, char *message);
// void failureCallback(uintptr_t channelPtr, char *errMessage, RequestErrorType errType);
// void successCallback(void *channelPtr, char *message);
// void failureCallback(void *channelPtr, char *errMessage, RequestErrorType errType);
import "C"

import (
Expand All @@ -16,14 +16,30 @@ import (
"google.golang.org/protobuf/proto"
)

// BaseClient defines an interface for methods common to both [RedisClient] and [RedisClusterClient].
type BaseClient interface {
// Close terminates the client by closing all associated resources.
Close()
}

type payload struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. capitalize
  2. which payload? response? connection? command? consider renaming
  3. add a doc

value string
error error
}

//export successCallback
func successCallback(channelPtr C.uintptr_t, cResponse *C.char) {
// TODO: Implement when we implement the command logic
func successCallback(channelPtr unsafe.Pointer, cResponse *C.char) {
// TODO: call lib.rs function to free response
response := C.GoString(cResponse)
resultChannel := *(*chan payload)(channelPtr)
resultChannel <- payload{value: response, error: nil}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it sync or async operation?

}

//export failureCallback
func failureCallback(channelPtr C.uintptr_t, cErrorMessage *C.char, cErrorType C.RequestErrorType) {
// TODO: Implement when we implement the command logic
func failureCallback(channelPtr unsafe.Pointer, cErrorMessage *C.char, cErrorType C.RequestErrorType) {
// TODO: call lib.rs function to free response
resultChannel := *(*chan payload)(channelPtr)
resultChannel <- payload{value: "", error: goError(cErrorType, cErrorMessage)}
}

type clientConfiguration interface {
Expand Down Expand Up @@ -71,3 +87,39 @@ func (client *baseClient) Close() {
C.close_client(client.coreClient)
client.coreClient = nil
}

func (client *baseClient) executeCommand(requestType C.RequestType, args []string) (interface{}, error) {
if client.coreClient == nil {
return nil, &ClosingError{"The client is closed."}
}

cArgs := toCStrings(args)
defer freeCStrings(cArgs)

resultChannel := make(chan payload)
resultChannelPtr := uintptr(unsafe.Pointer(&resultChannel))

C.command(client.coreClient, C.uintptr_t(resultChannelPtr), requestType, C.uintptr_t(len(args)), &cArgs[0])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You changed callback singature to take channel pointer as void *. Should be here second argument be void * too?


payload := <-resultChannel
if payload.error != nil {
return nil, payload.error
}

return payload.value, nil
}

func toCStrings(args []string) []*C.char {
cArgs := make([]*C.char, len(args))
for i, arg := range args {
cString := C.CString(arg)
cArgs[i] = cString
}
return cArgs
}

func freeCStrings(cArgs []*C.char) {
for _, arg := range cArgs {
C.free(unsafe.Pointer(arg))
}
}
8 changes: 6 additions & 2 deletions go/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ func (config *RedisClientConfiguration) toProtobuf() *protobuf.ConnectionRequest
// WithAddress adds an address for a known node in the cluster to this configuration's list of addresses. WithAddress can be
// called multiple times to add multiple addresses to the list. If the server is in cluster mode the list can be partial, as
// the client will attempt to map out the cluster and find all nodes. If the server is in standalone mode, only nodes whose
// addresses were provided will be used by the client. For example:
// addresses were provided will be used by the client.
//
// For example:
//
// config := NewRedisClientConfiguration().
// WithAddress(&NodeAddress{
Expand Down Expand Up @@ -256,7 +258,9 @@ func (config *RedisClusterClientConfiguration) toProtobuf() *protobuf.Connection
// WithAddress adds an address for a known node in the cluster to this configuration's list of addresses. WithAddress can be
// called multiple times to add multiple addresses to the list. If the server is in cluster mode the list can be partial, as
// the client will attempt to map out the cluster and find all nodes. If the server is in standalone mode, only nodes whose
// addresses were provided will be used by the client. For example:
// addresses were provided will be used by the client.
//
// For example:
//
// config := NewRedisClusterClientConfiguration().
// WithAddress(&NodeAddress{
Expand Down
7 changes: 7 additions & 0 deletions go/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ type DisconnectError struct {

func (e *DisconnectError) Error() string { return e.msg }

// ClosingError is a Redis client error that indicates that the client has closed and is no longer usable.
type ClosingError struct {
msg string
}

func (e *ClosingError) Error() string { return e.msg }

func goError(cErrorType C.RequestErrorType, cErrorMessage *C.char) error {
msg := C.GoString(cErrorMessage)
switch cErrorType {
Expand Down
17 changes: 17 additions & 0 deletions go/api/standalone_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package api

// #cgo LDFLAGS: -L../target/release -lglide_rs
// #include "../lib.h"
import "C"

// RedisClient is a client used for connection to standalone Redis servers.
Expand All @@ -20,3 +22,18 @@ func NewRedisClient(config *RedisClientConfiguration) (*RedisClient, error) {

return &RedisClient{client}, nil
}

// CustomCommand executes a single command, specified by args, without checking inputs. Every part of the command, including

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider creating a list of interfaces as we did in java client.
Is it possible in go to split doc and implementation into different files?

// the command name and subcommands, should be added as a separate value in args. The returning value depends on the executed
// command.
//
// This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or
// that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering
// pub/sub mode on RESP2 connections) shouldn't be called using this function.
//
// For example, to return a list of all pub/sub clients:
//
// client.CustomCommand([]string{"CLIENT", "LIST","TYPE", "PUBSUB"})
func (client *RedisClient) CustomCommand(args []string) (interface{}, error) {
return client.executeCommand(C.CustomCommand, args)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add non-null and non-empty check for args

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
"testing"

"github.com/aws/glide-for-redis/go/glide/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)

Expand All @@ -20,6 +22,8 @@ type GlideTestSuite struct {
standalonePorts []int
clusterPorts []int
redisVersion string
clients []*api.RedisClient
clusterClients []*api.RedisClusterClient
}

func (suite *GlideTestSuite) SetupSuite() {
Expand Down Expand Up @@ -118,3 +122,30 @@ func TestGlideTestSuite(t *testing.T) {
func (suite *GlideTestSuite) TearDownSuite() {
runClusterManager(suite, []string{"stop", "--prefix", "redis-cluster", "--keep-folder"}, false)
}

func (suite *GlideTestSuite) TearDownTest() {
for _, client := range suite.clients {
client.Close()
}

for _, client := range suite.clusterClients {
client.Close()
}
}

func (suite *GlideTestSuite) defaultClient() *api.RedisClient {
config := api.NewRedisClientConfiguration().
WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}).
WithRequestTimeout(5000)
return suite.client(config)
}

func (suite *GlideTestSuite) client(config *api.RedisClientConfiguration) *api.RedisClient {
client, err := api.NewRedisClient(config)

assert.Nil(suite.T(), err)
assert.NotNil(suite.T(), client)

suite.clients = append(suite.clients, client)
return client
}
74 changes: 74 additions & 0 deletions go/integTest/standalone_commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0

package integTest

import (
"fmt"
"strings"

"github.com/aws/glide-for-redis/go/glide/api"

"github.com/stretchr/testify/assert"
)

func (suite *GlideTestSuite) TestCustomCommandInfo() {
client := suite.defaultClient()
result, err := client.CustomCommand([]string{"INFO"})

assert.Nil(suite.T(), err)
assert.IsType(suite.T(), "", result)
strResult := result.(string)
assert.True(suite.T(), strings.Contains(strResult, "# Stats"))
}

func (suite *GlideTestSuite) TestCustomCommandPing() {
client := suite.defaultClient()
result, err := client.CustomCommand([]string{"PING"})

assert.Nil(suite.T(), err)
assert.Equal(suite.T(), "PONG", result)
}

func (suite *GlideTestSuite) TestCustomCommandClientInfo() {
clientName := "TEST_CLIENT_NAME"
config := api.NewRedisClientConfiguration().
WithAddress(&api.NodeAddress{Port: suite.standalonePorts[0]}).
WithClientName(clientName)
client := suite.client(config)

result, err := client.CustomCommand([]string{"CLIENT", "INFO"})

assert.Nil(suite.T(), err)
assert.IsType(suite.T(), "", result)
strResult := result.(string)
assert.True(suite.T(), strings.Contains(strResult, fmt.Sprintf("name=%s", clientName)))
}

func (suite *GlideTestSuite) TestCustomCommand_invalidCommand() {
client := suite.defaultClient()
result, err := client.CustomCommand([]string{"pewpew"})

assert.Nil(suite.T(), result)
assert.NotNil(suite.T(), err)
assert.IsType(suite.T(), &api.RequestError{}, err)
}

func (suite *GlideTestSuite) TestCustomCommand_invalidArgs() {
client := suite.defaultClient()
result, err := client.CustomCommand([]string{"ping", "pang", "pong"})

assert.Nil(suite.T(), result)
assert.NotNil(suite.T(), err)
assert.IsType(suite.T(), &api.RequestError{}, err)
}

func (suite *GlideTestSuite) TestCustomCommand_closedClient() {
client := suite.defaultClient()
client.Close()

result, err := client.CustomCommand([]string{"ping"})

assert.Nil(suite.T(), result)
assert.NotNil(suite.T(), err)
assert.IsType(suite.T(), &api.ClosingError{}, err)
}
95 changes: 93 additions & 2 deletions go/lib.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ typedef struct ConnectionResponse {
* `channel_address` is the address of the Go channel used by the callback to send the error message back to the caller of the command.
* `message` is the value returned by the Redis command.
*/
typedef void (*SuccessCallback)(uintptr_t channel_address, const char *message);
typedef void (*SuccessCallback)(const void *channel_address, const char *message);

/**
* Failure callback that is called when a Redis command fails.
Expand All @@ -41,7 +41,7 @@ typedef void (*SuccessCallback)(uintptr_t channel_address, const char *message);
* `error_message` is the error message returned by Redis for the failed command. It should be manually freed after this callback is invoked, otherwise a memory leak will occur.
* `error_type` is the type of error returned by glide-core, depending on the `RedisError` returned.
*/
typedef void (*FailureCallback)(uintptr_t channel_address,
typedef void (*FailureCallback)(const void *channel_address,
const char *error_message,
RequestErrorType error_type);

Expand Down Expand Up @@ -96,3 +96,94 @@ void close_client(const void *client_ptr);
* * The contained `connection_error_message` must not be null.
*/
void free_connection_response(struct ConnectionResponse *connection_response_ptr);

enum RequestType {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to move enum(s) to another file(s) and #include them?

On another hand we have permission to use cbindgen which can generate entire header automatically.

CustomCommand = 1,
GetString = 2,
SetString = 3,
Ping = 4,
Info = 5,
Del = 6,
Select = 7,
ConfigGet = 8,
ConfigSet = 9,
ConfigResetStat = 10,
ConfigRewrite = 11,
ClientGetName = 12,
ClientGetRedir = 13,
ClientId = 14,
ClientInfo = 15,
ClientKill = 16,
ClientList = 17,
ClientNoEvict = 18,
ClientNoTouch = 19,
ClientPause = 20,
ClientReply = 21,
ClientSetInfo = 22,
ClientSetName = 23,
ClientUnblock = 24,
ClientUnpause = 25,
Expire = 26,
HashSet = 27,
HashGet = 28,
HashDel = 29,
HashExists = 30,
MGet = 31,
MSet = 32,
Incr = 33,
IncrBy = 34,
Decr = 35,
IncrByFloat = 36,
DecrBy = 37,
HashGetAll = 38,
HashMSet = 39,
HashMGet = 40,
HashIncrBy = 41,
HashIncrByFloat = 42,
LPush = 43,
LPop = 44,
RPush = 45,
RPop = 46,
LLen = 47,
LRem = 48,
LRange = 49,
LTrim = 50,
SAdd = 51,
SRem = 52,
SMembers = 53,
SCard = 54,
PExpireAt = 55,
PExpire = 56,
ExpireAt = 57,
Exists = 58,
Unlink = 59,
TTL = 60,
Zadd = 61,
Zrem = 62,
Zrange = 63,
Zcard = 64,
Zcount = 65,
ZIncrBy = 66,
ZScore = 67,
Type = 68,
HLen = 69,
Echo = 70,
ZPopMin = 71,
Strlen = 72,
Lindex = 73,
ZPopMax = 74,
XRead = 75,
XAdd = 76,
XReadGroup = 77,
XAck = 78,
XTrim = 79,
XGroupCreate = 80,
XGroupDestroy = 81,
};
typedef uint32_t RequestType;

void command(const void *client_ptr,
uintptr_t channel,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe

Suggested change
uintptr_t channel,
void * channel,

RequestType command_type,
uintptr_t arg_count,
const char *const *args);
Loading
Loading