Skip to content

Commit 65eeb0f

Browse files
author
Runze Cui
committed
[DBS-000] fix lua script error handling
1 parent ce5d08f commit 65eeb0f

3 files changed

Lines changed: 64 additions & 21 deletions

File tree

internal/proto/redis_errors.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,11 +290,23 @@ func IsReadOnlyError(err error) bool {
290290
}
291291
// Check if wrapped error is a RedisError with READONLY prefix
292292
var redisErr RedisError
293-
if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "READONLY ") {
294-
return true
293+
if errors.As(err, &redisErr) {
294+
errMsg := redisErr.Error()
295+
if strings.HasPrefix(errMsg, "READONLY ") {
296+
return true
297+
}
298+
// For Lua scripts, the read-only error string contains "-READONLY" rather than beginning with "READONLY "
299+
if strings.Contains(errMsg, "-READONLY ") {
300+
return true
301+
}
295302
}
296303
// Fallback to string checking for backward compatibility
297-
return strings.HasPrefix(err.Error(), "READONLY ")
304+
errMsg := err.Error()
305+
if strings.HasPrefix(errMsg, "READONLY ") {
306+
return true
307+
}
308+
// For Lua scripts, the read-only error string contains "-READONLY" rather than beginning with "READONLY "
309+
return strings.Contains(errMsg, "-READONLY ")
298310
}
299311

300312
// IsMovedError checks if an error is a MovedError, even if wrapped.

internal/proto/redis_errors_test.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import (
99
// TestTypedRedisErrors tests that typed Redis errors are created correctly
1010
func TestTypedRedisErrors(t *testing.T) {
1111
tests := []struct {
12-
name string
13-
errorMsg string
14-
expectedType interface{}
15-
expectedMsg string
16-
checkFunc func(error) bool
17-
extractAddr func(error) string
12+
name string
13+
errorMsg string
14+
expectedType interface{}
15+
expectedMsg string
16+
checkFunc func(error) bool
17+
extractAddr func(error) string
1818
}{
1919
{
2020
name: "LOADING error",
@@ -30,6 +30,13 @@ func TestTypedRedisErrors(t *testing.T) {
3030
expectedMsg: "READONLY You can't write against a read only replica",
3131
checkFunc: IsReadOnlyError,
3232
},
33+
{
34+
name: "READONLY error from Lua script",
35+
errorMsg: "ERR Error running script (call to f_xxx): @user_script:1: -READONLY You can't write against a read only replica",
36+
expectedType: RedisError(""), // Lua script errors are parsed as generic RedisError (string type)
37+
expectedMsg: "ERR Error running script (call to f_xxx): @user_script:1: -READONLY You can't write against a read only replica",
38+
checkFunc: IsReadOnlyError, // But IsReadOnlyError should still detect it
39+
},
3340
{
3441
name: "MOVED error",
3542
errorMsg: "MOVED 3999 127.0.0.1:6381",
@@ -144,8 +151,17 @@ func TestTypedRedisErrors(t *testing.T) {
144151
}
145152

146153
// Check error type using errors.As
147-
if !errors.As(err, &tt.expectedType) {
148-
t.Errorf("Error type mismatch: expected %T, got %T", tt.expectedType, err)
154+
// Special handling for RedisError (string type) - use type assertion instead
155+
if _, ok := tt.expectedType.(RedisError); ok {
156+
// For RedisError, check using type assertion
157+
if _, ok := err.(RedisError); !ok {
158+
t.Errorf("Error type mismatch: expected RedisError, got %T", err)
159+
}
160+
} else {
161+
// For other types, use errors.As
162+
if !errors.As(err, &tt.expectedType) {
163+
t.Errorf("Error type mismatch: expected %T, got %T", tt.expectedType, err)
164+
}
149165
}
150166

151167
// Check using the helper function
@@ -181,6 +197,11 @@ func TestWrappedTypedErrors(t *testing.T) {
181197
errorMsg: "READONLY You can't write against a read only replica",
182198
checkFunc: IsReadOnlyError,
183199
},
200+
{
201+
name: "Wrapped READONLY error from Lua script",
202+
errorMsg: "ERR Error running script (call to f_xxx): @user_script:1: -READONLY You can't write against a read only replica",
203+
checkFunc: IsReadOnlyError,
204+
},
184205
{
185206
name: "Wrapped CLUSTERDOWN error",
186207
errorMsg: "CLUSTERDOWN The cluster is down",
@@ -389,4 +410,3 @@ func TestBackwardCompatibility(t *testing.T) {
389410
})
390411
}
391412
}
392-

redis.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -623,12 +623,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
623623
return nil
624624
}
625625

626-
func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {
627-
// Note: ReportResult is now called in withConn for circuit breaker limiters
628-
// We only call it here for non-circuit-breaker limiters or when called from _withConn
629-
// Since _withConn is only called from executeWithCircuitBreaker or when limiter is nil,
630-
// we skip reporting here to avoid double-reporting
631-
626+
func (c *baseClient) _releaseConn(ctx context.Context, cn *pool.Conn, err error) {
632627
if isBadConn(err, false, c.opt.Addr) {
633628
c.connPool.Remove(ctx, cn, err)
634629
} else {
@@ -640,6 +635,13 @@ func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error)
640635
}
641636
}
642637

638+
func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {
639+
if c.opt.Limiter != nil {
640+
c.opt.Limiter.ReportResult(err)
641+
}
642+
c._releaseConn(ctx, cn, err)
643+
}
644+
643645
func (c *baseClient) withConn(
644646
ctx context.Context, fn func(context.Context, *pool.Conn) error,
645647
) error {
@@ -661,14 +663,17 @@ func (c *baseClient) withConn(
661663
func (c *baseClient) _withConn(
662664
ctx context.Context, fn func(context.Context, *pool.Conn) error,
663665
) error {
664-
cn, err := c.getConn(ctx)
666+
// Use _getConn/_releaseConn to bypass limiter checks.
667+
// Limiter is handled by the caller (withConn) to avoid double-checking
668+
// when called from within circuit breaker Execute.
669+
cn, err := c._getConn(ctx)
665670
if err != nil {
666671
return err
667672
}
668673

669674
var fnErr error
670675
defer func() {
671-
c.releaseConn(ctx, cn, fnErr)
676+
c._releaseConn(ctx, cn, fnErr)
672677
}()
673678

674679
fnErr = fn(ctx, cn)
@@ -696,13 +701,19 @@ func (c *baseClient) executeWithCircuitBreaker(
696701
}
697702
}
698703

704+
// Execute with circuit breaker protection.
705+
// _withConn uses _getConn/_releaseConn which bypass the limiter,
706+
// avoiding double-checking since Allow() was already called in withConn().
699707
return cbLimiter.Execute(func() error {
700708
return c._withConn(ctx, fn)
701709
})
702710
}
703711

704712
func (c *baseClient) preConnect(ctx context.Context) error {
705-
cn, err := c.getConn(ctx)
713+
// Use _getConn to bypass limiter checks.
714+
// This is called from executeWithCircuitBreaker after Allow() has already passed,
715+
// to test connectivity before cb.Do() runs.
716+
cn, err := c._getConn(ctx)
706717
if err != nil {
707718
return err
708719
}

0 commit comments

Comments
 (0)