Skip to content
Merged
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
14 changes: 14 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
if proto.IsTryAgainError(err) {
return true
}
if proto.IsNoReplicasError(err) {
return true
}

// Fallback to string checking for backward compatibility with plain errors
s := err.Error()
Expand All @@ -145,6 +148,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
if strings.HasPrefix(s, "MASTERDOWN ") {
return true
}
if strings.HasPrefix(s, "NOREPLICAS ") {
return true
}

return false
}
Expand Down Expand Up @@ -342,6 +348,14 @@ func IsOOMError(err error) bool {
return proto.IsOOMError(err)
}

// IsNoReplicasError checks if an error is a Redis NOREPLICAS error, even if wrapped.
// NOREPLICAS errors occur when not enough replicas acknowledge a write operation.
// This typically happens with WAIT/WAITAOF commands or CLUSTER SETSLOT with synchronous
// replication when the required number of replicas cannot confirm the write within the timeout.
func IsNoReplicasError(err error) bool {
return proto.IsNoReplicasError(err)
}

//------------------------------------------------------------------------------

type timeoutError interface {
Expand Down
3 changes: 2 additions & 1 deletion error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ var _ = Describe("error", func() {
proto.ParseErrorReply([]byte("-READONLY You can't write against a read only replica")): true,
proto.ParseErrorReply([]byte("-CLUSTERDOWN The cluster is down")): true,
proto.ParseErrorReply([]byte("-TRYAGAIN Command cannot be processed, please try again")): true,
proto.ParseErrorReply([]byte("-ERR other")): false,
proto.ParseErrorReply([]byte("-NOREPLICAS Not enough good replicas to write")): true,
proto.ParseErrorReply([]byte("-ERR other")): false,
}

for err, expected := range data {
Expand Down
14 changes: 10 additions & 4 deletions error_wrapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,10 @@ func TestErrorWrappingInHookScenario(t *testing.T) {
// TestShouldRetryWithTypedErrors tests that shouldRetry works with typed errors
func TestShouldRetryWithTypedErrors(t *testing.T) {
tests := []struct {
name string
errorMsg string
shouldRetry bool
retryTimeout bool
name string
errorMsg string
shouldRetry bool
retryTimeout bool
}{
{
name: "LOADING error should retry",
Expand Down Expand Up @@ -280,6 +280,12 @@ func TestShouldRetryWithTypedErrors(t *testing.T) {
shouldRetry: true,
retryTimeout: false,
},
{
name: "NOREPLICAS error should retry",
errorMsg: "NOREPLICAS Not enough good replicas to write",
shouldRetry: true,
retryTimeout: false,
},
}

for _, tt := range tests {
Expand Down
39 changes: 39 additions & 0 deletions internal/proto/redis_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,25 @@ func NewOOMError(msg string) *OOMError {
return &OOMError{msg: msg}
}

// NoReplicasError is returned when not enough replicas acknowledge a write.
// This error occurs when using WAIT/WAITAOF commands or CLUSTER SETSLOT with
// synchronous replication, and the required number of replicas cannot confirm
// the write within the timeout period.
type NoReplicasError struct {
msg string
}

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

func (e *NoReplicasError) RedisError() {}

// NewNoReplicasError creates a new NoReplicasError with the given message.
func NewNoReplicasError(msg string) *NoReplicasError {
return &NoReplicasError{msg: msg}
}

// parseTypedRedisError parses a Redis error message and returns a typed error if applicable.
// This function maintains backward compatibility by keeping the same error messages.
func parseTypedRedisError(msg string) error {
Expand All @@ -235,6 +254,8 @@ func parseTypedRedisError(msg string) error {
return NewTryAgainError(msg)
case strings.HasPrefix(msg, "MASTERDOWN "):
return NewMasterDownError(msg)
case strings.HasPrefix(msg, "NOREPLICAS "):
return NewNoReplicasError(msg)
case msg == "ERR max number of clients reached":
return NewMaxClientsError(msg)
case strings.HasPrefix(msg, "NOAUTH "), strings.HasPrefix(msg, "WRONGPASS "), strings.Contains(msg, "unauthenticated"):
Expand Down Expand Up @@ -486,3 +507,21 @@ func IsOOMError(err error) bool {
// Fallback to string checking for backward compatibility
return strings.HasPrefix(err.Error(), "OOM ")
}

// IsNoReplicasError checks if an error is a NoReplicasError, even if wrapped.
func IsNoReplicasError(err error) bool {
if err == nil {
return false
}
var noReplicasErr *NoReplicasError
if errors.As(err, &noReplicasErr) {
return true
}
// Check if wrapped error is a RedisError with NOREPLICAS prefix
var redisErr RedisError
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "NOREPLICAS ") {
return true
}
// Fallback to string checking for backward compatibility
return strings.HasPrefix(err.Error(), "NOREPLICAS ")
}
20 changes: 13 additions & 7 deletions internal/proto/redis_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
// TestTypedRedisErrors tests that typed Redis errors are created correctly
func TestTypedRedisErrors(t *testing.T) {
tests := []struct {
name string
errorMsg string
expectedType interface{}
expectedMsg string
checkFunc func(error) bool
extractAddr func(error) string
name string
errorMsg string
expectedType interface{}
expectedMsg string
checkFunc func(error) bool
extractAddr func(error) string
}{
{
name: "LOADING error",
Expand Down Expand Up @@ -132,6 +132,13 @@ func TestTypedRedisErrors(t *testing.T) {
expectedMsg: "OOM command not allowed when used memory > 'maxmemory'",
checkFunc: IsOOMError,
},
{
name: "NOREPLICAS error",
errorMsg: "NOREPLICAS Not enough good replicas to write",
expectedType: &NoReplicasError{},
expectedMsg: "NOREPLICAS Not enough good replicas to write",
checkFunc: IsNoReplicasError,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -389,4 +396,3 @@ func TestBackwardCompatibility(t *testing.T) {
})
}
}

Loading