package redisstate import ( "context" "errors" "fmt" "slices" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/service/acceptgenericdelivery" "galaxy/mail/internal/service/listattempts" "galaxy/mail/internal/service/listdeliveries" "galaxy/mail/internal/service/resenddelivery" "github.com/redis/go-redis/v9" ) // OperatorStore provides the Redis-backed durable storage used by the // operator read and resend workflows. type OperatorStore struct { client *redis.Client writer *AtomicWriter keys Keyspace } // NewOperatorStore constructs one Redis-backed operator store. func NewOperatorStore(client *redis.Client) (*OperatorStore, error) { if client == nil { return nil, errors.New("new operator store: nil redis client") } writer, err := NewAtomicWriter(client) if err != nil { return nil, fmt.Errorf("new operator store: %w", err) } return &OperatorStore{ client: client, writer: writer, keys: Keyspace{}, }, nil } // GetDelivery loads one accepted delivery by its identifier. func (store *OperatorStore) GetDelivery(ctx context.Context, deliveryID common.DeliveryID) (deliverydomain.Delivery, bool, error) { if store == nil || store.client == nil { return deliverydomain.Delivery{}, false, errors.New("get operator delivery: nil store") } if ctx == nil { return deliverydomain.Delivery{}, false, errors.New("get operator delivery: nil context") } if err := deliveryID.Validate(); err != nil { return deliverydomain.Delivery{}, false, fmt.Errorf("get operator delivery: %w", err) } payload, err := store.client.Get(ctx, store.keys.Delivery(deliveryID)).Bytes() switch { case errors.Is(err, redis.Nil): return deliverydomain.Delivery{}, false, nil case err != nil: return deliverydomain.Delivery{}, false, fmt.Errorf("get operator delivery: %w", err) } record, err := UnmarshalDelivery(payload) if err != nil { return deliverydomain.Delivery{}, false, fmt.Errorf("get operator delivery: %w", err) } return record, true, nil } // GetDeadLetter loads the dead-letter entry associated with deliveryID when // one exists. func (store *OperatorStore) GetDeadLetter(ctx context.Context, deliveryID common.DeliveryID) (deliverydomain.DeadLetterEntry, bool, error) { if store == nil || store.client == nil { return deliverydomain.DeadLetterEntry{}, false, errors.New("get operator dead-letter entry: nil store") } if ctx == nil { return deliverydomain.DeadLetterEntry{}, false, errors.New("get operator dead-letter entry: nil context") } if err := deliveryID.Validate(); err != nil { return deliverydomain.DeadLetterEntry{}, false, fmt.Errorf("get operator dead-letter entry: %w", err) } payload, err := store.client.Get(ctx, store.keys.DeadLetter(deliveryID)).Bytes() switch { case errors.Is(err, redis.Nil): return deliverydomain.DeadLetterEntry{}, false, nil case err != nil: return deliverydomain.DeadLetterEntry{}, false, fmt.Errorf("get operator dead-letter entry: %w", err) } entry, err := UnmarshalDeadLetter(payload) if err != nil { return deliverydomain.DeadLetterEntry{}, false, fmt.Errorf("get operator dead-letter entry: %w", err) } return entry, true, nil } // GetDeliveryPayload loads one raw accepted attachment bundle by delivery id. func (store *OperatorStore) GetDeliveryPayload(ctx context.Context, deliveryID common.DeliveryID) (acceptgenericdelivery.DeliveryPayload, bool, error) { if store == nil || store.client == nil { return acceptgenericdelivery.DeliveryPayload{}, false, errors.New("get operator delivery payload: nil store") } if ctx == nil { return acceptgenericdelivery.DeliveryPayload{}, false, errors.New("get operator delivery payload: nil context") } if err := deliveryID.Validate(); err != nil { return acceptgenericdelivery.DeliveryPayload{}, false, fmt.Errorf("get operator delivery payload: %w", err) } payload, err := store.client.Get(ctx, store.keys.DeliveryPayload(deliveryID)).Bytes() switch { case errors.Is(err, redis.Nil): return acceptgenericdelivery.DeliveryPayload{}, false, nil case err != nil: return acceptgenericdelivery.DeliveryPayload{}, false, fmt.Errorf("get operator delivery payload: %w", err) } record, err := UnmarshalDeliveryPayload(payload) if err != nil { return acceptgenericdelivery.DeliveryPayload{}, false, fmt.Errorf("get operator delivery payload: %w", err) } return record, true, nil } // ListAttempts loads exactly expectedCount attempts in ascending attempt // number order. Missing attempts are treated as durable-state corruption. func (store *OperatorStore) ListAttempts(ctx context.Context, deliveryID common.DeliveryID, expectedCount int) ([]attempt.Attempt, error) { if store == nil || store.client == nil { return nil, errors.New("list operator attempts: nil store") } if ctx == nil { return nil, errors.New("list operator attempts: nil context") } if err := deliveryID.Validate(); err != nil { return nil, fmt.Errorf("list operator attempts: %w", err) } if expectedCount < 0 { return nil, errors.New("list operator attempts: negative expected count") } if expectedCount == 0 { return []attempt.Attempt{}, nil } result := make([]attempt.Attempt, 0, expectedCount) for attemptNo := 1; attemptNo <= expectedCount; attemptNo++ { payload, err := store.client.Get(ctx, store.keys.Attempt(deliveryID, attemptNo)).Bytes() switch { case errors.Is(err, redis.Nil): return nil, fmt.Errorf("list operator attempts: missing attempt %d for delivery %q", attemptNo, deliveryID) case err != nil: return nil, fmt.Errorf("list operator attempts: %w", err) } record, err := UnmarshalAttempt(payload) if err != nil { return nil, fmt.Errorf("list operator attempts: %w", err) } result = append(result, record) } return result, nil } // List loads one filtered ordered page of delivery records. func (store *OperatorStore) List(ctx context.Context, input listdeliveries.Input) (listdeliveries.Result, error) { if store == nil || store.client == nil { return listdeliveries.Result{}, errors.New("list operator deliveries: nil store") } if ctx == nil { return listdeliveries.Result{}, errors.New("list operator deliveries: nil context") } if err := input.Validate(); err != nil { return listdeliveries.Result{}, fmt.Errorf("list operator deliveries: %w", err) } selection := chooseListIndex(store.keys, input.Filters) if selection.mergeIDempotency { return store.listMergedIdempotency(ctx, input, selection.keys) } return store.listSingleIndex(ctx, input, selection.keys[0]) } // CreateResend atomically creates the cloned delivery, its first attempt, and // the optional cloned raw payload bundle. func (store *OperatorStore) CreateResend(ctx context.Context, input resenddelivery.CreateResendInput) error { if store == nil || store.client == nil || store.writer == nil { return errors.New("create operator resend: nil store") } if ctx == nil { return errors.New("create operator resend: nil context") } if err := input.Validate(); err != nil { return fmt.Errorf("create operator resend: %w", err) } writerInput := CreateAcceptanceInput{ Delivery: input.Delivery, FirstAttempt: &input.FirstAttempt, } if input.DeliveryPayload != nil { writerInput.DeliveryPayload = input.DeliveryPayload } if err := store.writer.CreateAcceptance(ctx, writerInput); err != nil { return fmt.Errorf("create operator resend: %w", err) } return nil } type listSelection struct { keys []string mergeIDempotency bool } func chooseListIndex(keyspace Keyspace, filters listdeliveries.Filters) listSelection { switch { case filters.IdempotencyKey != "" && filters.Source != "": return listSelection{ keys: []string{keyspace.IdempotencyIndex(filters.Source, filters.IdempotencyKey)}, } case filters.IdempotencyKey != "": return listSelection{ keys: []string{ keyspace.IdempotencyIndex(deliverydomain.SourceAuthSession, filters.IdempotencyKey), keyspace.IdempotencyIndex(deliverydomain.SourceNotification, filters.IdempotencyKey), keyspace.IdempotencyIndex(deliverydomain.SourceOperatorResend, filters.IdempotencyKey), }, mergeIDempotency: true, } case filters.Recipient != "": return listSelection{keys: []string{keyspace.RecipientIndex(filters.Recipient)}} case filters.TemplateID != "": return listSelection{keys: []string{keyspace.TemplateIndex(filters.TemplateID)}} case filters.Status != "": return listSelection{keys: []string{keyspace.StatusIndex(filters.Status)}} case filters.Source != "": return listSelection{keys: []string{keyspace.SourceIndex(filters.Source)}} default: return listSelection{keys: []string{keyspace.CreatedAtIndex()}} } } func (store *OperatorStore) listSingleIndex(ctx context.Context, input listdeliveries.Input, indexKey string) (listdeliveries.Result, error) { startIndex := int64(0) if input.Cursor != nil { cursorIndex, err := cursorStartIndex(ctx, store.client, indexKey, *input.Cursor) if err != nil { return listdeliveries.Result{}, err } startIndex = cursorIndex } items, nextCursor, err := store.collectFromIndex(ctx, indexKey, startIndex, input.Limit, input.Filters) if err != nil { return listdeliveries.Result{}, err } return listdeliveries.Result{ Items: items, NextCursor: nextCursor, }, nil } func (store *OperatorStore) listMergedIdempotency(ctx context.Context, input listdeliveries.Input, indexKeys []string) (listdeliveries.Result, error) { iterators := make([]*redisIndexIterator, 0, len(indexKeys)) for _, key := range indexKeys { iterators = append(iterators, &redisIndexIterator{ client: store.client, indexKey: key, batchSize: listBatchSize(input.Limit), cursor: input.Cursor, }) } heads := make([]indexedRef, 0, len(iterators)) for index, iterator := range iterators { ref, err := iterator.Next(ctx) if err != nil { return listdeliveries.Result{}, err } if ref != nil { heads = append(heads, indexedRef{streamIndex: index, ref: *ref}) } } items := make([]deliverydomain.Delivery, 0, input.Limit+1) for len(heads) > 0 && len(items) <= input.Limit { bestIndex := 0 for index := 1; index < len(heads); index++ { if compareDeliveryOrder(heads[index].ref, heads[bestIndex].ref) < 0 { bestIndex = index } } selected := heads[bestIndex] heads = slices.Delete(heads, bestIndex, bestIndex+1) record, found, err := store.GetDelivery(ctx, selected.ref.DeliveryID) if err != nil { return listdeliveries.Result{}, err } if found && input.Filters.Matches(record) { items = append(items, record) } nextRef, err := iterators[selected.streamIndex].Next(ctx) if err != nil { return listdeliveries.Result{}, err } if nextRef != nil { heads = append(heads, indexedRef{streamIndex: selected.streamIndex, ref: *nextRef}) } } result := listdeliveries.Result{} if len(items) > input.Limit { next := cursorFromDelivery(items[input.Limit-1]) result.NextCursor = &next items = items[:input.Limit] } result.Items = items return result, nil } func (store *OperatorStore) collectFromIndex( ctx context.Context, indexKey string, startIndex int64, limit int, filters listdeliveries.Filters, ) ([]deliverydomain.Delivery, *listdeliveries.Cursor, error) { items := make([]deliverydomain.Delivery, 0, limit+1) batchSize := listBatchSize(limit) for len(items) <= limit { batch, err := store.client.ZRevRangeWithScores(ctx, indexKey, startIndex, startIndex+int64(batchSize)-1).Result() if err != nil { return nil, nil, fmt.Errorf("list operator deliveries: %w", err) } if len(batch) == 0 { break } startIndex += int64(len(batch)) for _, member := range batch { deliveryID, err := memberDeliveryID(member.Member) if err != nil { return nil, nil, fmt.Errorf("list operator deliveries: %w", err) } record, found, err := store.GetDelivery(ctx, deliveryID) if err != nil { return nil, nil, err } if !found || !filters.Matches(record) { continue } items = append(items, record) if len(items) > limit { break } } } var nextCursor *listdeliveries.Cursor if len(items) > limit { next := cursorFromDelivery(items[limit-1]) nextCursor = &next items = items[:limit] } return items, nextCursor, nil } type indexedRef struct { streamIndex int ref deliveryRef } type deliveryRef struct { CreatedAt time.Time DeliveryID common.DeliveryID } type redisIndexIterator struct { client *redis.Client indexKey string batchSize int offset int64 cursor *listdeliveries.Cursor batch []redis.Z position int } func (iterator *redisIndexIterator) Next(ctx context.Context) (*deliveryRef, error) { for { if iterator.position >= len(iterator.batch) { batch, err := iterator.client.ZRevRangeWithScores( ctx, iterator.indexKey, iterator.offset, iterator.offset+int64(iterator.batchSize)-1, ).Result() if err != nil { return nil, fmt.Errorf("list operator deliveries: %w", err) } if len(batch) == 0 { return nil, nil } iterator.batch = batch iterator.position = 0 iterator.offset += int64(len(batch)) } ref, err := deliveryRefFromSortedSet(iterator.batch[iterator.position]) iterator.position++ if err != nil { return nil, fmt.Errorf("list operator deliveries: %w", err) } if iterator.cursor != nil && !isAfterCursor(ref, *iterator.cursor) { continue } return &ref, nil } } func cursorStartIndex(ctx context.Context, client *redis.Client, indexKey string, cursor listdeliveries.Cursor) (int64, error) { score, err := client.ZScore(ctx, indexKey, cursor.DeliveryID.String()).Result() switch { case errors.Is(err, redis.Nil): return 0, listdeliveries.ErrInvalidCursor case err != nil: return 0, fmt.Errorf("list operator deliveries: %w", err) } if !time.UnixMilli(int64(score)).UTC().Equal(cursor.CreatedAt.UTC()) { return 0, listdeliveries.ErrInvalidCursor } rank, err := client.ZRevRank(ctx, indexKey, cursor.DeliveryID.String()).Result() switch { case errors.Is(err, redis.Nil): return 0, listdeliveries.ErrInvalidCursor case err != nil: return 0, fmt.Errorf("list operator deliveries: %w", err) default: return rank + 1, nil } } func compareDeliveryOrder(left deliveryRef, right deliveryRef) int { switch { case left.CreatedAt.After(right.CreatedAt): return -1 case left.CreatedAt.Before(right.CreatedAt): return 1 case left.DeliveryID.String() > right.DeliveryID.String(): return -1 case left.DeliveryID.String() < right.DeliveryID.String(): return 1 default: return 0 } } func isAfterCursor(ref deliveryRef, cursor listdeliveries.Cursor) bool { return compareDeliveryOrder(ref, deliveryRef{ CreatedAt: cursor.CreatedAt.UTC(), DeliveryID: cursor.DeliveryID, }) > 0 } func cursorFromDelivery(record deliverydomain.Delivery) listdeliveries.Cursor { return listdeliveries.Cursor{ CreatedAt: record.CreatedAt.UTC(), DeliveryID: record.DeliveryID, } } func deliveryRefFromSortedSet(member redis.Z) (deliveryRef, error) { deliveryID, err := memberDeliveryID(member.Member) if err != nil { return deliveryRef{}, err } return deliveryRef{ CreatedAt: time.UnixMilli(int64(member.Score)).UTC(), DeliveryID: deliveryID, }, nil } func memberDeliveryID(member any) (common.DeliveryID, error) { value, ok := member.(string) if !ok { return "", fmt.Errorf("unexpected delivery index member type %T", member) } deliveryID := common.DeliveryID(value) if err := deliveryID.Validate(); err != nil { return "", fmt.Errorf("delivery index member delivery id: %w", err) } return deliveryID, nil } func listBatchSize(limit int) int { size := limit * 4 if size < limit+1 { size = limit + 1 } if size < 100 { size = 100 } return size } var _ listdeliveries.Store = (*OperatorStore)(nil) var _ listattempts.Store = (*OperatorStore)(nil) var _ resenddelivery.Store = (*OperatorStore)(nil)