feat: mail service
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user