-
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 17 commits
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
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. consider using linter for makefile too (later) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,16 @@ | ||
install-tools-go1.18: | ||
go install github.com/vakenbolt/[email protected] | ||
go install google.golang.org/protobuf/cmd/[email protected] | ||
go install mvdan.cc/[email protected] | ||
go install github.com/segmentio/[email protected] | ||
go install honnef.co/go/tools/cmd/[email protected] | ||
|
||
install-tools: | ||
@cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % | ||
go install github.com/vakenbolt/[email protected] | ||
Yury-Fridlyand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
go install google.golang.org/protobuf/cmd/[email protected] | ||
go install mvdan.cc/[email protected] | ||
go install github.com/segmentio/[email protected] | ||
go install honnef.co/go/tools/cmd/[email protected] | ||
|
||
build: build-glide-core build-glide-client generate-protobuf | ||
go build ./... | ||
|
@@ -23,6 +34,12 @@ generate-protobuf: | |
lint: | ||
go vet ./... | ||
staticcheck ./... | ||
if [ "$$(gofumpt -l . | wc -l)" -gt 0 ]; then exit 1; fi | ||
if [ "$$(golines -l --shorten-comments -m 127 . | wc -l)" -gt 0 ]; then exit 1; fi | ||
|
||
format: | ||
gofumpt -w . | ||
golines -w --shorten-comments -m 127 . | ||
|
||
unit-test-report: | ||
mkdir -p reports | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,320 @@ | ||
/** | ||
aaron-congo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 | ||
*/ | ||
|
||
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 // Optional: if not supplied, "localhost" will be used. | ||
Port *uint32 // Optional: if not supplied, 6379 will be used. | ||
} | ||
|
||
const ( | ||
defaultHost = "localhost" | ||
defaultPort = uint32(6379) | ||
) | ||
|
||
// RedisCredentials represents the credentials for connecting to a Redis server. | ||
type RedisCredentials struct { | ||
// Optional: the username that will be used for authenticating connections to the Redis servers. If not supplied, "default" | ||
// will be used. | ||
Username *string | ||
// Required: the password that will be used for authenticating connections to the Redis servers. | ||
aaron-congo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Password *string | ||
} | ||
|
||
func (credentials *RedisCredentials) validate() error { | ||
if credentials.Password == nil { | ||
Yury-Fridlyand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return &RedisError{ | ||
"RedisCredentials.Password evaluated to nil. Password must be non-nil to authenticate with credentials.", | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// ReadFrom represents the client's read from strategy. | ||
type ReadFrom int | ||
|
||
const ( | ||
// Primary - Always get from primary, in order to get the freshest data. | ||
Primary ReadFrom = 0 | ||
aaron-congo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// PreferReplica - Spread the requests between all replicas in a round-robin manner. If no replica is available, route the | ||
// requests to the primary. | ||
PreferReplica 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 *uint32 | ||
} | ||
|
||
func (config *baseClientConfiguration) toProtobufConnRequest() (*protobuf.ConnectionRequest, error) { | ||
request := protobuf.ConnectionRequest{} | ||
for _, address := range config.addresses { | ||
if address.Host == nil { | ||
host := defaultHost | ||
address.Host = &host | ||
} | ||
|
||
if address.Port == nil { | ||
port := defaultPort | ||
address.Port = &port | ||
} | ||
|
||
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 { | ||
if err := config.credentials.validate(); err != nil { | ||
return nil, err | ||
} | ||
|
||
authInfo := protobuf.AuthenticationInfo{} | ||
if config.credentials.Username != nil { | ||
Yury-Fridlyand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
authInfo.Username = *config.credentials.Username | ||
} | ||
authInfo.Password = *config.credentials.Password | ||
request.AuthenticationInfo = &authInfo | ||
} | ||
|
||
request.ReadFrom = mapReadFrom(config.readFrom) | ||
if config.requestTimeout != nil { | ||
request.RequestTimeout = *config.requestTimeout | ||
} | ||
|
||
return &request, nil | ||
} | ||
|
||
func mapReadFrom(readFrom ReadFrom) protobuf.ReadFrom { | ||
if readFrom == PreferReplica { | ||
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 | ||
aaron-congo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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 { | ||
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. @aaron-congo can you confirm that the formatting for these comments is preserved when generating documentation? 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. The line length is not preserved, but blank lines and indents are. So docs for the above look like this (you can get them by running
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. What about font for the coded section and "N". Can we use backticks (like Python) or add html tags (like 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. With Go docs you create code blocks by indenting the relevant code, like I've done here. There is no inline code highlighting like you would get with backticks. When you upload the package the site will automatically render the Go docs into html, which includes rendering the indented code blocks as <pre> blocks |
||
// Required: 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 | ||
// Required: the multiplier that will be applied to the waiting time between each retry. | ||
Factor *uint32 | ||
// Required: the exponent base configured for the strategy. | ||
ExponentBase *uint32 | ||
} | ||
|
||
func (strategy *BackoffStrategy) validate() error { | ||
if strategy.NumOfRetries == nil { | ||
return &RedisError{ | ||
"BackoffStrategy.NumOfRetries evaluated to nil. NumOfRetries must be non-nil to use a BackoffStrategy.", | ||
} | ||
} | ||
|
||
if strategy.Factor == nil { | ||
return &RedisError{ | ||
"BackoffStrategy.Factor evaluated to nil. Factor must be non-nil to use a BackoffStrategy.", | ||
} | ||
} | ||
|
||
if strategy.ExponentBase == nil { | ||
return &RedisError{ | ||
"BackoffStrategy.ExponentBase evaluated to nil. ExponentBase must be non-nil to use a BackoffStrategy.", | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// 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 *uint32 | ||
} | ||
|
||
// 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{} | ||
} | ||
|
||
func (config *RedisClientConfiguration) toProtobufConnRequest() (*protobuf.ConnectionRequest, error) { | ||
request, err := config.baseClientConfiguration.toProtobufConnRequest() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
request.ClusterModeEnabled = false | ||
if config.reconnectStrategy != nil { | ||
if err = config.reconnectStrategy.validate(); err != nil { | ||
return nil, err | ||
} | ||
|
||
request.ConnectionRetryStrategy = &protobuf.ConnectionRetryStrategy{ | ||
NumberOfRetries: *config.reconnectStrategy.NumOfRetries, | ||
Factor: *config.reconnectStrategy.Factor, | ||
ExponentBase: *config.reconnectStrategy.ExponentBase, | ||
} | ||
} | ||
|
||
if config.databaseId != nil { | ||
request.DatabaseId = *config.databaseId | ||
} | ||
|
||
return request, nil | ||
} | ||
|
||
// 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 = &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 = &id | ||
return config | ||
} | ||
|
||
// RedisClusterClientConfiguration represents the configuration settings for a Cluster Redis client. | ||
// Note: 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, error) { | ||
request, err := config.baseClientConfiguration.toProtobufConnRequest() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
request.ClusterModeEnabled = true | ||
return request, nil | ||
} | ||
|
||
// 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. | ||
Yury-Fridlyand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 = &requestTimeout | ||
return config | ||
} |
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.
Having one set of CI tool versions was not working for me because go 1.18 and go 1.21 require different versions of CI tools. Old tools did not work on go 1.21 and new tools did not work on go 1.18. I've looked around and have not been able to find recommendations on how to handle this scenario. The solution I've landed on so far is to have different make targets for installing tools on go 1.18 vs go 1.20