@@ -272,6 +272,7 @@ func (r *RedisIndex) Add(ctx context.Context, engineKeys, requestKeys []BlockHas
272272 return err
273273 }
274274 pipe .HSet (ctx , redisKey , field , "" )
275+ pipe .SAdd (ctx , redisPodEntriesKey (entry .PodIdentifier ), redisPodEntryMember (redisKey , field ))
275276 }
276277 }
277278
@@ -330,6 +331,7 @@ func (r *RedisIndex) evictPodsFromRequestKey(ctx context.Context, requestKey Blo
330331 return err
331332 }
332333 pipe .HDel (ctx , redisKey , field )
334+ pipe .SRem (ctx , redisPodEntriesKey (entry .PodIdentifier ), redisPodEntryMember (redisKey , field ))
333335 }
334336
335337 if _ , err := pipe .Exec (ctx ); err != nil {
@@ -408,60 +410,79 @@ func redisEngineKey(engineKey BlockHash) string {
408410 return "engine:" + engineKey .String ()
409411}
410412
411- // Clear removes every hash field for the pod across all request-key hashes and
412- // device tiers. Each field is a JSON-encoded PodEntry, so matching decodes the
413- // field and compares PodIdentifier — catching every tier, group, and speculative
414- // variant. It pages the keyspace with SCAN (skipping engine: keys), HDELs the
415- // pod's fields, and prunes now-empty hashes. O(keyspace), but Clear is rare and
416- // off the Lookup/Add hot path. Because it deletes from the shared store, it is
417- // correct for multi-replica deployments with no cross-process coordination.
413+ func redisPodEntriesKey (podIdentifier string ) string {
414+ return "kvblock:pod:" + podIdentifier + ":entries"
415+ }
416+
417+ func redisPodEntryMember (requestKey , field string ) string {
418+ return requestKey + "\x00 " + field
419+ }
420+
421+ type redisPodEntryRef struct {
422+ requestKey string
423+ field string
424+ }
425+
426+ func parseRedisPodEntryMember (member string ) (redisPodEntryRef , bool ) {
427+ requestKey , field , ok := strings .Cut (member , "\x00 " )
428+ if ! ok || requestKey == "" || field == "" {
429+ return redisPodEntryRef {}, false
430+ }
431+ return redisPodEntryRef {requestKey : requestKey , field : field }, true
432+ }
433+
434+ // Clear removes every reverse-indexed field for the pod across all request-key
435+ // hashes and device tiers. Add maintains an exact per-pod set of
436+ // "<requestKey>\x00<encodedPodField>" entries, so Clear only touches keys this
437+ // index recorded for that pod instead of scanning the whole Redis keyspace.
418438func (r * RedisIndex ) Clear (ctx context.Context , podIdentifier string ) error {
419439 logger := log .FromContext (ctx ).WithName ("kvblock.RedisIndex.Clear" )
440+ podEntriesKey := redisPodEntriesKey (podIdentifier )
420441
421442 const scanBatch int64 = 1024
422- removed := 0
443+ processed := 0
423444 var cursor uint64
424445 for {
425- keys , next , err := r .RedisClient .Scan (ctx , cursor , "* " , scanBatch ).Result ()
446+ members , next , err := r .RedisClient .SScan (ctx , podEntriesKey , cursor , "" , scanBatch ).Result ()
426447 if err != nil {
427- return fmt .Errorf ("clear scan failed: %w" , err )
448+ return fmt .Errorf ("clear reverse-index scan failed: %w" , err )
428449 }
429- for _ , key := range keys {
430- if strings .HasPrefix (key , "engine:" ) {
431- continue // engine:<hash> ZSETs hold no pod fields
432- }
433-
434- fields , err := r .RedisClient .HKeys (ctx , key ).Result ()
435- if err != nil {
436- return fmt .Errorf ("clear hkeys failed for %s: %w" , key , err )
437- }
438-
439- var stale []string
440- for _ , field := range fields {
441- if entry , ok := decodeRedisPodField (field ); ok && entry .PodIdentifier == podIdentifier {
442- stale = append (stale , field )
443- }
444- }
445- if len (stale ) == 0 {
446- continue
447- }
448-
449- if err := r .RedisClient .HDel (ctx , key , stale ... ).Err (); err != nil {
450- return fmt .Errorf ("clear hdel failed for %s: %w" , key , err )
451- }
452- removed += len (stale )
453-
454- if err := pruneRequestKeyScript .Run (ctx , r .RedisClient , []string {key }).Err (); err != nil &&
455- ! errors .Is (err , redis .Nil ) {
456- return fmt .Errorf ("clear prune failed for %s: %w" , key , err )
457- }
450+ if err := r .clearRedisPodEntryMembers (ctx , podEntriesKey , members ); err != nil {
451+ return err
458452 }
453+ processed += len (members )
459454
460- if cursor = next ; cursor == 0 {
455+ if cursor = next ; cursor != 0 {
456+ continue
457+ }
458+ remaining , err := r .RedisClient .SCard (ctx , podEntriesKey ).Result ()
459+ if err != nil {
460+ return fmt .Errorf ("clear reverse-index cardinality failed: %w" , err )
461+ }
462+ if remaining == 0 {
461463 break
462464 }
463465 }
464466
465- logger .Info ("cleared pod from index" , "pod" , podIdentifier , "removed" , removed )
467+ logger .Info ("cleared pod from index" , "pod" , podIdentifier , "members" , processed )
468+ return nil
469+ }
470+
471+ func (r * RedisIndex ) clearRedisPodEntryMembers (ctx context.Context , podEntriesKey string , members []string ) error {
472+ if len (members ) == 0 {
473+ return nil
474+ }
475+
476+ pipe := r .RedisClient .Pipeline ()
477+ for _ , member := range members {
478+ ref , ok := parseRedisPodEntryMember (member )
479+ if ok {
480+ pipe .HDel (ctx , ref .requestKey , ref .field )
481+ }
482+ pipe .SRem (ctx , podEntriesKey , member )
483+ }
484+ if _ , err := pipe .Exec (ctx ); err != nil {
485+ return fmt .Errorf ("clear reverse-index pipeline failed: %w" , err )
486+ }
466487 return nil
467488}
0 commit comments