package redisstate import ( "context" "errors" "fmt" "galaxy/mail/internal/domain/malformedcommand" "github.com/redis/go-redis/v9" ) // MalformedCommandStore provides the Redis-backed storage used for // operator-visible malformed async command records. type MalformedCommandStore struct { client *redis.Client keys Keyspace } // NewMalformedCommandStore constructs one Redis-backed malformed-command // store. func NewMalformedCommandStore(client *redis.Client) (*MalformedCommandStore, error) { if client == nil { return nil, errors.New("new malformed command store: nil redis client") } return &MalformedCommandStore{ client: client, keys: Keyspace{}, }, nil } // Record stores entry idempotently by stream entry id. func (store *MalformedCommandStore) Record(ctx context.Context, entry malformedcommand.Entry) error { if store == nil || store.client == nil { return errors.New("record malformed command: nil store") } if ctx == nil { return errors.New("record malformed command: nil context") } if err := entry.Validate(); err != nil { return fmt.Errorf("record malformed command: %w", err) } payload, err := MarshalMalformedCommand(entry) if err != nil { return fmt.Errorf("record malformed command: %w", err) } key := store.keys.MalformedCommand(entry.StreamEntryID) indexKey := store.keys.MalformedCommandCreatedAtIndex() score := float64(entry.RecordedAt.UTC().UnixMilli()) watchErr := store.client.Watch(ctx, func(tx *redis.Tx) error { exists, err := tx.Exists(ctx, key).Result() if err != nil { return fmt.Errorf("record malformed command: %w", err) } if exists > 0 { return nil } _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { pipe.Set(ctx, key, payload, DeadLetterTTL) pipe.ZAdd(ctx, indexKey, redis.Z{ Score: score, Member: entry.StreamEntryID, }) return nil }) if err != nil { return fmt.Errorf("record malformed command: %w", err) } return nil }, key) switch { case errors.Is(watchErr, redis.TxFailedErr): return nil case watchErr != nil: return watchErr default: return nil } } // Get loads one malformed-command entry by stream entry id. func (store *MalformedCommandStore) Get(ctx context.Context, streamEntryID string) (malformedcommand.Entry, bool, error) { if store == nil || store.client == nil { return malformedcommand.Entry{}, false, errors.New("get malformed command: nil store") } if ctx == nil { return malformedcommand.Entry{}, false, errors.New("get malformed command: nil context") } payload, err := store.client.Get(ctx, store.keys.MalformedCommand(streamEntryID)).Bytes() switch { case errors.Is(err, redis.Nil): return malformedcommand.Entry{}, false, nil case err != nil: return malformedcommand.Entry{}, false, fmt.Errorf("get malformed command: %w", err) } entry, err := UnmarshalMalformedCommand(payload) if err != nil { return malformedcommand.Entry{}, false, fmt.Errorf("get malformed command: %w", err) } return entry, true, nil }