fix(limiter): recover redis script after flush (#6486)

- Retry the Lua rate limiter with EVAL when Redis returns NOSCRIPT
- Add regression coverage for Redis script cache flush recovery
This commit is contained in:
BradleyZ
2026-10-20 22:28:33 +08:00
committed by GitHub
parent a8535fac10
commit e2a0c4b16a
2 changed files with 95 additions and 0 deletions
+15
View File
@@ -4,6 +4,7 @@ import (
"context"
_ "embed"
"fmt"
"strings"
"sync"
"github.com/QuantumNous/new-api/common"
@@ -63,6 +64,20 @@ func (rl *RedisLimiter) Allow(ctx context.Context, key string, opts ...Option) (
).Int()
if err != nil {
if strings.HasPrefix(err.Error(), "NOSCRIPT ") {
result, err := rl.client.Eval(
ctx,
rateLimitScript,
[]string{key},
config.Requested,
config.Rate,
config.Capacity,
).Int()
if err != nil {
return false, fmt.Errorf("rate limit failed: %w", err)
}
return result == 1, nil
}
return false, fmt.Errorf("rate limit failed: %w", err)
}
return result == 1, nil
+80
View File
@@ -0,0 +1,80 @@
package limiter
import (
"context"
"sync"
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func useLimiterMiniRedis(t *testing.T) (*miniredis.Miniredis, *redis.Client) {
t.Helper()
previousInstance := instance
redisServer := miniredis.RunT(t)
redisClient := redis.NewClient(&redis.Options{Addr: redisServer.Addr()})
require.NoError(t, redisClient.Ping(context.Background()).Err())
instance = nil
once = sync.Once{}
t.Cleanup(func() {
_ = redisClient.Close()
instance = previousInstance
once = sync.Once{}
if previousInstance != nil {
once.Do(func() {})
}
})
return redisServer, redisClient
}
func TestAllowFallsBackToEvalAfterScriptFlush(t *testing.T) {
ctx := context.Background()
_, redisClient := useLimiterMiniRedis(t)
redisLimiter := New(ctx, redisClient)
allowed, err := redisLimiter.Allow(
ctx,
"rateLimit:test-before-flush",
WithCapacity(60),
WithRate(60),
WithRequested(60),
)
require.NoError(t, err)
assert.True(t, allowed)
exists, err := redisClient.ScriptExists(ctx, redisLimiter.limitScriptSHA).Result()
require.NoError(t, err)
require.Len(t, exists, 1)
assert.True(t, exists[0])
require.NoError(t, redisClient.ScriptFlush(ctx).Err())
exists, err = redisClient.ScriptExists(ctx, redisLimiter.limitScriptSHA).Result()
require.NoError(t, err)
require.Len(t, exists, 1)
assert.False(t, exists[0])
allowed, err = redisLimiter.Allow(
ctx,
"rateLimit:test-after-flush",
WithCapacity(60),
WithRate(60),
WithRequested(60),
)
require.NoError(t, err)
assert.True(t, allowed)
exists, err = redisClient.ScriptExists(ctx, redisLimiter.limitScriptSHA).Result()
require.NoError(t, err)
require.Len(t, exists, 1)
assert.True(t, exists[0])
}