test(redis): paginate SCAN in the Clear failure test

miniredis 2.39 ignores the SCAN COUNT hint and returns every key in one
batch. The test now paginates SCAN in its own hook, so it no longer
depends on miniredis batching.
This commit is contained in:
2026-10-04 10:14:04 +01:00
parent 9b70f5693a
commit 67cc71bbf8
+26 -18
View File
@@ -2,6 +2,8 @@ package libpack_cache_redis
import ( import (
"fmt" "fmt"
"sort"
"strconv"
"sync/atomic" "sync/atomic"
"testing" "testing"
@@ -125,19 +127,9 @@ func TestClear_NormalPrefix_StillClearsAllOwnedKeys(t *testing.T) {
// intercept DEL commands without a rewrite of RedisConfig into an // intercept DEL commands without a rewrite of RedisConfig into an
// interface-backed mock, which the task's infra allows. // interface-backed mock, which the task's infra allows.
// //
// Every DEL call is made to fail (not just one). A test that lets only one // Every DEL call is made to fail so the key set never shrinks between
// batch's DEL succeed was tried first, and does not work against miniredis: // scans. SCAN is paginated by the hook itself because miniredis ignores
// its SCAN implementation uses an index-based cursor over a sorted key // COUNT and returns every key in one batch.
// snapshot (cmd_generic.go cmdScan), which is invalidated as soon as a
// preceding batch actually deletes keys mid-scan (the very "scan, then
// delete this batch" loop Clear uses), causing miniredis to abort the SCAN
// early with a synthetic empty result before Clear's loop reaches the next
// batch. Real Redis's SCAN cursor (reverse binary iteration) is documented
// to be robust against concurrent deletion during a single full iteration
// and would not exhibit this; it is a miniredis simplification, not a
// RedisConfig defect. Failing every batch's DEL sidesteps it (no batch is
// ever actually deleted, so the key set never shrinks between scans) while
// still proving Clear does not stop scanning after the first DEL error.
func TestClear_AllBatchesDelFail_AttemptsEveryBatchAndAggregatesErrors(t *testing.T) { func TestClear_AllBatchesDelFail_AttemptsEveryBatchAndAggregatesErrors(t *testing.T) {
s, err := miniredis.Run() s, err := miniredis.Run()
require.NoError(t, err) require.NoError(t, err)
@@ -146,10 +138,7 @@ func TestClear_AllBatchesDelFail_AttemptsEveryBatchAndAggregatesErrors(t *testin
rc, err := New(&RedisClientConfig{RedisServer: s.Addr(), Prefix: "pfx:"}) rc, err := New(&RedisClientConfig{RedisServer: s.Addr(), Prefix: "pfx:"})
require.NoError(t, err) require.NoError(t, err)
// miniredis SCAN batches deterministically by COUNT over the sorted, // 250 owned keys with a SCAN page size of 100 yield exactly 3 batches.
// match-filtered key set (see cmd_generic.go cmdScan), so 250 owned keys
// with a SCAN COUNT of 100 (as used by Clear) yields exactly 3 batches:
// [0:100), [100:200), [200:250).
const totalKeys = 250 const totalKeys = 250
const wantBatches = 3 const wantBatches = 3
for i := 0; i < totalKeys; i++ { for i := 0; i < totalKeys; i++ {
@@ -158,7 +147,26 @@ func TestClear_AllBatchesDelFail_AttemptsEveryBatchAndAggregatesErrors(t *testin
} }
var seen int32 var seen int32
s.Server().SetPreHook(func(c *miniredis_server.Peer, cmd string, _ ...string) bool { const scanPage = 100
s.Server().SetPreHook(func(c *miniredis_server.Peer, cmd string, args ...string) bool {
if cmd == "SCAN" {
cursor, convErr := strconv.Atoi(args[0])
if convErr != nil {
c.WriteError("ERR invalid cursor")
return true
}
keys := s.Keys()
sort.Strings(keys)
end := min(cursor+scanPage, len(keys))
next := end
if end == len(keys) {
next = 0
}
c.WriteLen(2)
c.WriteBulk(strconv.Itoa(next))
c.WriteStrings(keys[cursor:end])
return true
}
if cmd != "DEL" { if cmd != "DEL" {
return false // let miniredis handle everything else normally return false // let miniredis handle everything else normally
} }