mirror of
https://github.com/redis/go-redis.git
synced 2025-12-18 23:34:11 +03:00
fix(retry): Add retry mechanism for NOREPLICAS error (#3647)
This commit is contained in:
14
error.go
14
error.go
@@ -124,6 +124,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
|
|||||||
if proto.IsTryAgainError(err) {
|
if proto.IsTryAgainError(err) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if proto.IsNoReplicasError(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to string checking for backward compatibility with plain errors
|
// Fallback to string checking for backward compatibility with plain errors
|
||||||
s := err.Error()
|
s := err.Error()
|
||||||
@@ -145,6 +148,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
|
|||||||
if strings.HasPrefix(s, "MASTERDOWN ") {
|
if strings.HasPrefix(s, "MASTERDOWN ") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(s, "NOREPLICAS ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -342,6 +348,14 @@ func IsOOMError(err error) bool {
|
|||||||
return proto.IsOOMError(err)
|
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 {
|
type timeoutError interface {
|
||||||
|
|||||||
@@ -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("-READONLY You can't write against a read only replica")): true,
|
||||||
proto.ParseErrorReply([]byte("-CLUSTERDOWN The cluster is down")): 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("-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 {
|
for err, expected := range data {
|
||||||
|
|||||||
@@ -239,10 +239,10 @@ func TestErrorWrappingInHookScenario(t *testing.T) {
|
|||||||
// TestShouldRetryWithTypedErrors tests that shouldRetry works with typed errors
|
// TestShouldRetryWithTypedErrors tests that shouldRetry works with typed errors
|
||||||
func TestShouldRetryWithTypedErrors(t *testing.T) {
|
func TestShouldRetryWithTypedErrors(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
errorMsg string
|
errorMsg string
|
||||||
shouldRetry bool
|
shouldRetry bool
|
||||||
retryTimeout bool
|
retryTimeout bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "LOADING error should retry",
|
name: "LOADING error should retry",
|
||||||
@@ -280,6 +280,12 @@ func TestShouldRetryWithTypedErrors(t *testing.T) {
|
|||||||
shouldRetry: true,
|
shouldRetry: true,
|
||||||
retryTimeout: false,
|
retryTimeout: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "NOREPLICAS error should retry",
|
||||||
|
errorMsg: "NOREPLICAS Not enough good replicas to write",
|
||||||
|
shouldRetry: true,
|
||||||
|
retryTimeout: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -212,6 +212,25 @@ func NewOOMError(msg string) *OOMError {
|
|||||||
return &OOMError{msg: msg}
|
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.
|
// parseTypedRedisError parses a Redis error message and returns a typed error if applicable.
|
||||||
// This function maintains backward compatibility by keeping the same error messages.
|
// This function maintains backward compatibility by keeping the same error messages.
|
||||||
func parseTypedRedisError(msg string) error {
|
func parseTypedRedisError(msg string) error {
|
||||||
@@ -235,6 +254,8 @@ func parseTypedRedisError(msg string) error {
|
|||||||
return NewTryAgainError(msg)
|
return NewTryAgainError(msg)
|
||||||
case strings.HasPrefix(msg, "MASTERDOWN "):
|
case strings.HasPrefix(msg, "MASTERDOWN "):
|
||||||
return NewMasterDownError(msg)
|
return NewMasterDownError(msg)
|
||||||
|
case strings.HasPrefix(msg, "NOREPLICAS "):
|
||||||
|
return NewNoReplicasError(msg)
|
||||||
case msg == "ERR max number of clients reached":
|
case msg == "ERR max number of clients reached":
|
||||||
return NewMaxClientsError(msg)
|
return NewMaxClientsError(msg)
|
||||||
case strings.HasPrefix(msg, "NOAUTH "), strings.HasPrefix(msg, "WRONGPASS "), strings.Contains(msg, "unauthenticated"):
|
case strings.HasPrefix(msg, "NOAUTH "), strings.HasPrefix(msg, "WRONGPASS "), strings.Contains(msg, "unauthenticated"):
|
||||||
@@ -486,3 +507,21 @@ func IsOOMError(err error) bool {
|
|||||||
// Fallback to string checking for backward compatibility
|
// Fallback to string checking for backward compatibility
|
||||||
return strings.HasPrefix(err.Error(), "OOM ")
|
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 ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
// TestTypedRedisErrors tests that typed Redis errors are created correctly
|
// TestTypedRedisErrors tests that typed Redis errors are created correctly
|
||||||
func TestTypedRedisErrors(t *testing.T) {
|
func TestTypedRedisErrors(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
errorMsg string
|
errorMsg string
|
||||||
expectedType interface{}
|
expectedType interface{}
|
||||||
expectedMsg string
|
expectedMsg string
|
||||||
checkFunc func(error) bool
|
checkFunc func(error) bool
|
||||||
extractAddr func(error) string
|
extractAddr func(error) string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "LOADING error",
|
name: "LOADING error",
|
||||||
@@ -132,6 +132,13 @@ func TestTypedRedisErrors(t *testing.T) {
|
|||||||
expectedMsg: "OOM command not allowed when used memory > 'maxmemory'",
|
expectedMsg: "OOM command not allowed when used memory > 'maxmemory'",
|
||||||
checkFunc: IsOOMError,
|
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 {
|
for _, tt := range tests {
|
||||||
@@ -389,4 +396,3 @@ func TestBackwardCompatibility(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user