package redisstate import ( "context" "errors" "fmt" "strings" "galaxy/mail/internal/domain/common" "github.com/redis/go-redis/v9" ) // CleanupReport describes the work done by IndexCleaner. type CleanupReport struct { // ScannedIndexes stores how many secondary index keys were inspected. ScannedIndexes int // ScannedMembers stores how many index members were examined. ScannedMembers int // RemovedMembers stores how many stale members were removed. RemovedMembers int } // IndexCleaner removes stale delivery references from the Mail Service // secondary indexes after primary delivery keys expire by TTL. type IndexCleaner struct { client *redis.Client keyspace Keyspace } // NewIndexCleaner constructs one delivery-index cleanup helper. func NewIndexCleaner(client *redis.Client) (*IndexCleaner, error) { if client == nil { return nil, errors.New("new redis index cleaner: nil client") } return &IndexCleaner{ client: client, keyspace: Keyspace{}, }, nil } // CleanDeliveryIndexes scans every `mail:idx:*` key and removes members that // no longer have a primary delivery record. func (cleaner *IndexCleaner) CleanDeliveryIndexes(ctx context.Context) (CleanupReport, error) { if cleaner == nil || cleaner.client == nil { return CleanupReport{}, errors.New("clean delivery indexes in redis: nil cleaner") } if ctx == nil { return CleanupReport{}, errors.New("clean delivery indexes in redis: nil context") } var ( report CleanupReport cursor uint64 ) for { keys, nextCursor, err := cleaner.client.Scan(ctx, cursor, cleaner.keyspace.SecondaryIndexPattern(), 0).Result() if err != nil { return CleanupReport{}, fmt.Errorf("clean delivery indexes in redis: %w", err) } for _, key := range keys { if key == cleaner.keyspace.MalformedCommandCreatedAtIndex() { continue } report.ScannedIndexes++ members, err := cleaner.client.ZRange(ctx, key, 0, -1).Result() if err != nil { return CleanupReport{}, fmt.Errorf("clean delivery indexes in redis: read index %q: %w", key, err) } report.ScannedMembers += len(members) for _, member := range members { remove, err := cleaner.shouldRemoveMember(ctx, member) if err != nil { return CleanupReport{}, fmt.Errorf("clean delivery indexes in redis: inspect index %q member %q: %w", key, member, err) } if !remove { continue } if err := cleaner.client.ZRem(ctx, key, member).Err(); err != nil { return CleanupReport{}, fmt.Errorf("clean delivery indexes in redis: remove index %q member %q: %w", key, member, err) } report.RemovedMembers++ } } if nextCursor == 0 { return report, nil } cursor = nextCursor } } func (cleaner *IndexCleaner) shouldRemoveMember(ctx context.Context, member string) (bool, error) { if strings.TrimSpace(member) == "" { return true, nil } deliveryID := common.DeliveryID(member) if err := deliveryID.Validate(); err != nil { return true, nil } exists, err := cleaner.client.Exists(ctx, cleaner.keyspace.Delivery(deliveryID)).Result() if err != nil { return false, err } return exists == 0, nil }