-
Notifications
You must be signed in to change notification settings - Fork 0
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
Implement Redis client configuration logic #104
Changes from 1 commit
731bfbb
e4526af
364386f
111f656
6dfc602
5cf5784
9f76bad
28ead19
59c53e6
6cd71e4
2143942
dfe3e32
39177c2
e5ed300
43d9a13
7f5059d
e223baf
6ae23a1
8f12115
8b8971b
81c58af
0fab94a
7eb12fd
810791f
ae473a8
d046752
6301ca6
a62b9fe
50286f1
3422c91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
package api | ||
|
||
import "github.com/aws/glide-for-redis/go/glide/protobuf" | ||
|
||
// NodeAddress represents the host address and port of a node in the cluster. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently go doc comments are supposed to include the name of the type/func in the first sentence, so the docs are slightly different than the other clients to accommodate that. I've tried to keep them as close possible still though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Yury-Fridlyand present tense will be forced for Go it seems ;) |
||
type NodeAddress struct { | ||
Host string | ||
Port uint32 | ||
} | ||
|
||
// RedisCredentials represents the credentials for connecting to a Redis server. | ||
type RedisCredentials struct { | ||
// The username that will be used for authenticating connections to the Redis servers. If not | ||
// supplied, "default" will be used. | ||
Username string | ||
// The password that will be used for authenticating connections to the Redis servers. | ||
Password string | ||
} | ||
|
||
// ReadFrom represents the client's read from strategy. | ||
type ReadFrom int | ||
|
||
const ( | ||
// Always get from primary, in order to get the freshest data. | ||
PRIMARY ReadFrom = 0 | ||
// Spread the requests between all replicas in a round robin manner. If no replica is available, | ||
// route the requests to the primary. | ||
PREFER_REPLICA ReadFrom = 1 | ||
) | ||
|
||
type baseClientConfiguration struct { | ||
addresses []NodeAddress | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've made these fields private (indicated by the lower-case first letter) so that the user has to use the With* builder methods instead of accessing them directly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of putting docs for these private fields here, I've put the docs above the associated With* builder methods below |
||
useTLS bool | ||
credentials *RedisCredentials | ||
readFrom ReadFrom | ||
requestTimeout int32 | ||
} | ||
|
||
func (config *baseClientConfiguration) toProtobufConnRequest() *protobuf.ConnectionRequest { | ||
request := protobuf.ConnectionRequest{} | ||
for _, address := range config.addresses { | ||
nodeAddress := &protobuf.NodeAddress{ | ||
Host: address.Host, | ||
Port: address.Port, | ||
} | ||
request.Addresses = append(request.Addresses, nodeAddress) | ||
} | ||
|
||
if config.useTLS { | ||
request.TlsMode = protobuf.TlsMode_SecureTls | ||
} else { | ||
request.TlsMode = protobuf.TlsMode_NoTls | ||
} | ||
|
||
if config.credentials != nil { | ||
authInfo := protobuf.AuthenticationInfo{} | ||
if config.credentials.Username != "" { | ||
authInfo.Username = config.credentials.Username | ||
} | ||
authInfo.Password = config.credentials.Password | ||
request.AuthenticationInfo = &authInfo | ||
} | ||
|
||
request.ReadFrom = mapReadFrom(config.readFrom) | ||
if config.requestTimeout > 0 { | ||
request.RequestTimeout = uint32(config.requestTimeout) | ||
} | ||
|
||
return &request | ||
} | ||
|
||
func mapReadFrom(readFrom ReadFrom) protobuf.ReadFrom { | ||
if readFrom == PREFER_REPLICA { | ||
return protobuf.ReadFrom_PreferReplica | ||
} | ||
|
||
return protobuf.ReadFrom_Primary | ||
} | ||
|
||
// BackoffStrategy represents the strategy used to determine how and when to reconnect, in case of | ||
// connection failures. The time between attempts grows exponentially, to the formula | ||
// rand(0 ... factor * (exponentBase ^ N)), where N is the number of failed attempts. | ||
// | ||
// Once the maximum value is reached, that will remain the time between retry attempts until a | ||
// reconnect attempt is successful. The client will attempt to reconnect indefinitely. | ||
type BackoffStrategy struct { | ||
// Number of retry attempts that the client should perform when disconnected from the server, | ||
// where the time between retries increases. Once the retries have reached the maximum value, | ||
// the time between retries will remain constant until a reconnect attempt is successful. | ||
NumOfRetries uint32 | ||
// The multiplier that will be applied to the waiting time between each retry. | ||
Factor uint32 | ||
// The exponent base configured for the strategy. | ||
ExponentBase uint32 | ||
} | ||
|
||
// RedisClientConfiguration represents the configuration settings for a Standalone Redis client. | ||
// baseClientConfiguration is an embedded struct that contains shared settings for standalone and | ||
// cluster clients. | ||
type RedisClientConfiguration struct { | ||
baseClientConfiguration | ||
reconnectStrategy *BackoffStrategy | ||
acarbonetto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
databaseId int32 | ||
} | ||
|
||
// NewRedisClientConfiguration returns a [RedisClientConfiguration] with default configuration | ||
// settings. For further configuration, use the [RedisClientConfiguration] With* methods. | ||
func NewRedisClientConfiguration() *RedisClientConfiguration { | ||
aaron-congo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return &RedisClientConfiguration{ | ||
baseClientConfiguration: baseClientConfiguration{readFrom: PRIMARY}, | ||
} | ||
} | ||
|
||
func (config *RedisClientConfiguration) toProtobufConnRequest() *protobuf.ConnectionRequest { | ||
request := config.baseClientConfiguration.toProtobufConnRequest() | ||
request.ClusterModeEnabled = false | ||
if config.reconnectStrategy != nil { | ||
request.ConnectionRetryStrategy = &protobuf.ConnectionRetryStrategy{ | ||
NumberOfRetries: config.reconnectStrategy.NumOfRetries, | ||
Factor: config.reconnectStrategy.Factor, | ||
ExponentBase: config.reconnectStrategy.ExponentBase, | ||
} | ||
} | ||
|
||
if config.databaseId >= 0 { | ||
request.DatabaseId = uint32(config.databaseId) | ||
} | ||
|
||
return request | ||
} | ||
|
||
// 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: | ||
// | ||
// config := NewRedisClientConfiguration(). | ||
// WithAddress(&NodeAddress{ | ||
// Host: "sample-address-0001.use1.cache.amazonaws.com", Port: 6379}). | ||
// WithAddress(&NodeAddress{ | ||
// Host: "sample-address-0002.use1.cache.amazonaws.com", Port: 6379}) | ||
func (config *RedisClientConfiguration) WithAddress(address *NodeAddress) *RedisClientConfiguration { | ||
config.addresses = append(config.addresses, *address) | ||
return config | ||
} | ||
|
||
// WithUseTLS configures the TLS settings for this configuration. Set to true if communication with | ||
// the cluster should use Transport Level Security. This setting should match the TLS configuration | ||
// of the server/cluster, otherwise the connection attempt will fail. | ||
func (config *RedisClientConfiguration) WithUseTLS(useTLS bool) *RedisClientConfiguration { | ||
config.useTLS = useTLS | ||
return config | ||
} | ||
|
||
// WithCredentials sets the credentials for the authentication process. If none are set, the client | ||
// will not authenticate itself with the server. | ||
func (config *RedisClientConfiguration) WithCredentials(credentials *RedisCredentials) *RedisClientConfiguration { | ||
config.credentials = credentials | ||
return config | ||
} | ||
|
||
// WithReadFrom sets the client's [ReadFrom] strategy. If not set, [PRIMARY] will be used. | ||
func (config *RedisClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisClientConfiguration { | ||
config.readFrom = readFrom | ||
return config | ||
} | ||
|
||
// WithRequestTimeout sets the duration in milliseconds that the client should wait for a request to | ||
// complete. This duration encompasses sending the request, awaiting for a response from the server, | ||
// and any required reconnections or retries. If the specified timeout is exceeded for a pending | ||
// request, it will result in a timeout error. If not set, a default value will be used. | ||
func (config *RedisClientConfiguration) WithRequestTimeout(requestTimeout uint32) *RedisClientConfiguration { | ||
config.requestTimeout = int32(requestTimeout) | ||
return config | ||
} | ||
|
||
// WithReconnectStrategy sets the [BackoffStrategy] used to determine how and when to reconnect, in | ||
// case of connection failures. If not set, a default backoff strategy will be used. | ||
func (config *RedisClientConfiguration) WithReconnectStrategy(strategy *BackoffStrategy) *RedisClientConfiguration { | ||
config.reconnectStrategy = strategy | ||
return config | ||
} | ||
|
||
// WithDatabaseId sets the index of the logical database to connect to. | ||
func (config *RedisClientConfiguration) WithDatabaseId(id uint32) *RedisClientConfiguration { | ||
config.databaseId = int32(id) | ||
return config | ||
} | ||
|
||
// RedisClusterClientConfiguration represents the configuration settings for a Cluster Redis client. | ||
// Notes: Currently, the reconnection strategy in cluster mode is not configurable, and exponential | ||
// backoff with fixed values is used. | ||
type RedisClusterClientConfiguration struct { | ||
baseClientConfiguration | ||
} | ||
|
||
// NewRedisClusterClientConfiguration returns a [RedisClientConfiguration] with default | ||
// configuration settings. For further configuration, use the [RedisClientConfiguration] With* | ||
// methods. | ||
func NewRedisClusterClientConfiguration() *RedisClusterClientConfiguration { | ||
return &RedisClusterClientConfiguration{ | ||
baseClientConfiguration: baseClientConfiguration{}, | ||
} | ||
} | ||
|
||
func (config *RedisClusterClientConfiguration) toProtobufConnRequest() *protobuf.ConnectionRequest { | ||
Yury-Fridlyand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
request := config.baseClientConfiguration.toProtobufConnRequest() | ||
request.ClusterModeEnabled = true | ||
return request | ||
} | ||
|
||
// 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: | ||
// | ||
// config := NewRedisClusterClientConfiguration(). | ||
// WithAddress(&NodeAddress{ | ||
// Host: "sample-address-0001.use1.cache.amazonaws.com", Port: 6379}). | ||
// WithAddress(&NodeAddress{ | ||
// Host: "sample-address-0002.use1.cache.amazonaws.com", Port: 6379}) | ||
func (config *RedisClusterClientConfiguration) WithAddress(address NodeAddress) *RedisClusterClientConfiguration { | ||
config.addresses = append(config.addresses, address) | ||
return config | ||
} | ||
|
||
// WithUseTLS configures the TLS settings for this configuration. Set to true if communication with | ||
// the cluster should use Transport Level Security. This setting should match the TLS configuration | ||
// of the server/cluster, otherwise the connection attempt will fail. | ||
func (config *RedisClusterClientConfiguration) WithUseTLS(useTLS bool) *RedisClusterClientConfiguration { | ||
config.useTLS = useTLS | ||
return config | ||
} | ||
|
||
// WithCredentials sets the credentials for the authentication process. If none are set, the client | ||
// will not authenticate itself with the server. | ||
func (config *RedisClusterClientConfiguration) WithCredentials(credentials *RedisCredentials) *RedisClusterClientConfiguration { | ||
config.credentials = credentials | ||
return config | ||
} | ||
|
||
// WithReadFrom sets the client's [ReadFrom] strategy. If not set, [PRIMARY] will be used. | ||
func (config *RedisClusterClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisClusterClientConfiguration { | ||
config.readFrom = readFrom | ||
return config | ||
} | ||
|
||
// WithRequestTimeout sets the duration in milliseconds that the client should wait for a request to | ||
// complete. This duration encompasses sending the request, awaiting for a response from the server, | ||
// and any required reconnections or retries. If the specified timeout is exceeded for a pending | ||
// request, it will result in a timeout error. If not set, a default value will be used. | ||
func (config *RedisClusterClientConfiguration) WithRequestTimeout(requestTimeout uint32) *RedisClusterClientConfiguration { | ||
config.requestTimeout = int32(requestTimeout) | ||
return config | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package api | ||
|
||
import ( | ||
"github.com/aws/glide-for-redis/go/glide/protobuf" | ||
"github.com/stretchr/testify/assert" | ||
"testing" | ||
) | ||
|
||
func TestConnectionRequestProtobufGeneration_defaultStandaloneConfig(t *testing.T) { | ||
config := NewRedisClientConfiguration() | ||
expected := &protobuf.ConnectionRequest{ | ||
TlsMode: protobuf.TlsMode_NoTls, | ||
ClusterModeEnabled: false, | ||
ReadFrom: protobuf.ReadFrom_Primary, | ||
} | ||
|
||
result := config.toProtobufConnRequest() | ||
|
||
assert.Equal(t, expected, result) | ||
} | ||
|
||
func TestConnectionRequestProtobufGeneration_defaultClusterConfig(t *testing.T) { | ||
config := NewRedisClusterClientConfiguration() | ||
expected := &protobuf.ConnectionRequest{ | ||
TlsMode: protobuf.TlsMode_NoTls, | ||
ClusterModeEnabled: true, | ||
ReadFrom: protobuf.ReadFrom_Primary, | ||
} | ||
|
||
result := config.toProtobufConnRequest() | ||
|
||
assert.Equal(t, expected, result) | ||
} | ||
|
||
func TestConnectionRequestProtobufGeneration_allFieldsSet(t *testing.T) { | ||
hosts := []string{"host1", "host2"} | ||
Yury-Fridlyand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ports := []uint32{1234, 5678} | ||
username := "username" | ||
password := "password" | ||
var timeout uint32 = 3 | ||
var retries, factor, base uint32 = 5, 10, 50 | ||
var databaseId uint32 = 1 | ||
|
||
config := NewRedisClientConfiguration(). | ||
WithUseTLS(true). | ||
WithReadFrom(PREFER_REPLICA). | ||
WithCredentials(&RedisCredentials{username, password}). | ||
WithRequestTimeout(timeout). | ||
WithReconnectStrategy(&BackoffStrategy{retries, factor, base}). | ||
WithDatabaseId(databaseId) | ||
|
||
expected := &protobuf.ConnectionRequest{ | ||
TlsMode: protobuf.TlsMode_SecureTls, | ||
ReadFrom: protobuf.ReadFrom_PreferReplica, | ||
ClusterModeEnabled: false, | ||
AuthenticationInfo: &protobuf.AuthenticationInfo{Username: username, Password: password}, | ||
RequestTimeout: timeout, | ||
ConnectionRetryStrategy: &protobuf.ConnectionRetryStrategy{ | ||
NumberOfRetries: retries, | ||
Factor: factor, | ||
ExponentBase: base, | ||
}, | ||
DatabaseId: databaseId, | ||
} | ||
|
||
assert.Equal(t, len(hosts), len(ports)) | ||
for i := 0; i < len(hosts); i++ { | ||
config.WithAddress(&NodeAddress{hosts[i], ports[i]}) | ||
expected.Addresses = append( | ||
expected.Addresses, | ||
&protobuf.NodeAddress{Host: hosts[i], Port: ports[i]}, | ||
) | ||
} | ||
|
||
result := config.toProtobufConnRequest() | ||
|
||
assert.Equal(t, expected, result) | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider using linter for makefile too (later)