fix(kvblock): avoid full keyspace scan in Redis Clear#673
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR refactors RedisIndex.Clear to avoid scanning the entire Redis keyspace by maintaining a per-pod reverse index, and adds targeted tests to validate the new behavior.
Changes:
- Add a per-pod Redis SET reverse index updated from
AddandEvict. - Rewrite
Clearto operate viaSSCANover the per-pod reverse index rather thanSCANover all keys. - Add tests covering “clear doesn’t delete unrelated keys” and “evict removes reverse-index entries”.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| pkg/kvcache/kvblock/redis.go | Introduces reverse-index helpers; updates Add/eviction to maintain it; rewrites Clear to use it. |
| pkg/kvcache/kvblock/redis_test.go | Adds inspection helper and new tests validating Clear isolation and reverse-index pruning on eviction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| podEntriesKey := "kvblock:pod:" + pod.PodIdentifier + ":entries" | ||
| member := requestKey.String() + "\x00" + field | ||
|
|
||
| require.NoError(t, index.Add(ctx, nil, []BlockHash{requestKey}, []PodEntry{pod})) | ||
| ok, err := index.RedisClient.SIsMember(ctx, podEntriesKey, member).Result() | ||
| require.NoError(t, err) | ||
| require.True(t, ok) |
| members := make([]string, 0) | ||
| var cursor uint64 | ||
| for { | ||
| keys, next, err := r.RedisClient.Scan(ctx, cursor, "*", scanBatch).Result() | ||
| batch, next, err := r.RedisClient.SScan(ctx, podEntriesKey, cursor, "", scanBatch).Result() | ||
| if err != nil { | ||
| return fmt.Errorf("clear scan failed: %w", err) | ||
| return fmt.Errorf("clear reverse-index scan failed: %w", err) | ||
| } | ||
| for _, key := range keys { | ||
| if strings.HasPrefix(key, "engine:") { | ||
| continue // engine:<hash> ZSETs hold no pod fields | ||
| } | ||
|
|
||
| fields, err := r.RedisClient.HKeys(ctx, key).Result() | ||
| if err != nil { | ||
| return fmt.Errorf("clear hkeys failed for %s: %w", key, err) | ||
| } | ||
| members = append(members, batch...) | ||
| if cursor = next; cursor == 0 { | ||
| break | ||
| } | ||
| } |
| pipe.HDel(ctx, requestKey, field) | ||
| pipe.SRem(ctx, podEntriesKey, member) |
| if len(stale) == 0 { | ||
| const clearBatchSize = 1024 | ||
| for start := 0; start < len(members); start += clearBatchSize { | ||
| end := min(start+clearBatchSize, len(members)) |
c2251a9 to
cedb0bf
Compare
093d954 to
a9d2943
Compare
a9d2943 to
2aff0bf
Compare
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
33ce02a to
bfaeaa3
Compare
|
Hi @vMaroon, If you have bandwidth, would you mind taking a look at the Redis Thanks a lot! |
|
Following up on my +1 from #672 with an actual review -- I had mapped the same per-pod reverse-index design against the code before finding this PR, so this is the shape I believe in. Verified beyond reading:
Correctness walkthrough, all confirmed against the code:
The one design point worth a maintainer decision is the back-compat position (the question I'd raised on #672): index-authoritative plus a documented flush is defensible, and I see why fallback-to-SCAN is unattractive -- an empty reverse set is indistinguishable from "pre-upgrade entries exist unindexed", so a naive fallback would full-scan on every Clear of an already-clean pod and defeat the fix. But the failure mode when an operator misses the upgrade note is silent: a restarted pod that reuses its identity keeps its pre-upgrade fields, so Lookup keeps reporting cache hits for a now-cold pod. A cheap middle ground would be a one-time schema marker ( One non-blocking observation: LGTM from my side (not an approver). |
Summary
Optimize Redis/Valkey
Clearby maintaining a per-pod reverse index.Clear(ctx, podIdentifier)now scanskvblock:pod:<podIdentifier>:entriesand removes entries with pipelinedHDEL+SREM, instead of scanning the full Redis keyspace.Notes
Addrecords reverse-index members.Evictremoves reverse-index members.Cleardrops malformed reverse-index members and continues.Clearfor pre-upgrade entries.Addnow writes both the request-key hash and the per-pod reverse-index set, and the set member stores<requestKey>\0<encodedPodField>. This adds write and memory overhead in exchange for avoiding full keyspace scans duringClear.Benchmark
Real Redis
redis:7.4.1, persistence disabled:Testing
make lintgo test ./pkg/kvcache/kvblockgo test ./pkg/kveventsgo test ./tests/profiling/kv_cache_index -run '^$'go test -tags redis_real_bench ./tests/profiling/kv_cache_index -run '^$'go test -race ./pkg/kvcache/kvblock -run 'TestRedis|TestValkeyIndexBehavior/(ClearBasic|ClearIsolatesOtherPods|ClearAllTiers|ClearThenReAdd|StressHighCardinality)' -count=1Fixes #672