package redis // DeadLetterQueue provides a simple implementation of a dead letter queue using Redis sorted sets and hashes. Each failed job is stored as a JSON-encoded entry with metadata such as failure time, reason, and retry count. The sorted set allows for efficient retrieval of jobs in order of failure time, while the hash stores the detailed metadata for each job. import ( "context" "encoding/json" "fmt" "time" "github.com/redis/go-redis/v9" "stream.api/internal/database/model" ) const ( dlqKey = "render:jobs:dlq" dlqMetaPrefix = "render:jobs:dlq:meta:" ) type DeadLetterQueue struct { client *redis.Client } type DLQEntry struct { Job *model.Job `json:"job"` FailureTime time.Time `json:"failure_time"` Reason string `json:"reason"` RetryCount int64 `json:"retry_count"` } func NewDeadLetterQueue(client *redis.Client) *DeadLetterQueue { return &DeadLetterQueue{client: client} } // Add adds a failed job to the DLQ func (dlq *DeadLetterQueue) Add(ctx context.Context, job *model.Job, reason string) error { retryCount := int64(0) if job != nil && job.RetryCount != nil { retryCount = *job.RetryCount } entry := DLQEntry{ Job: job, FailureTime: time.Now(), Reason: reason, RetryCount: retryCount, } data, err := json.Marshal(entry) if err != nil { return fmt.Errorf("failed to marshal DLQ entry: %w", err) } // Add to sorted set with timestamp as score score := float64(time.Now().Unix()) if err := dlq.client.ZAdd(ctx, dlqKey, redis.Z{ Score: score, Member: job.ID, }).Err(); err != nil { return fmt.Errorf("failed to add to DLQ: %w", err) } // Store metadata metaKey := dlqMetaPrefix + job.ID if err := dlq.client.Set(ctx, metaKey, data, 0).Err(); err != nil { return fmt.Errorf("failed to store DLQ metadata: %w", err) } return nil } // Get retrieves a job from the DLQ func (dlq *DeadLetterQueue) Get(ctx context.Context, jobID string) (*DLQEntry, error) { metaKey := dlqMetaPrefix + jobID data, err := dlq.client.Get(ctx, metaKey).Bytes() if err != nil { if err == redis.Nil { return nil, fmt.Errorf("job not found in DLQ") } return nil, fmt.Errorf("failed to get DLQ entry: %w", err) } var entry DLQEntry if err := json.Unmarshal(data, &entry); err != nil { return nil, fmt.Errorf("failed to unmarshal DLQ entry: %w", err) } return &entry, nil } // List returns all jobs in the DLQ func (dlq *DeadLetterQueue) List(ctx context.Context, offset, limit int64) ([]*DLQEntry, error) { // Get job IDs from sorted set (newest first) jobIDs, err := dlq.client.ZRevRange(ctx, dlqKey, offset, offset+limit-1).Result() if err != nil { return nil, fmt.Errorf("failed to list DLQ: %w", err) } entries := make([]*DLQEntry, 0, len(jobIDs)) for _, jobID := range jobIDs { entry, err := dlq.Get(ctx, jobID) if err != nil { // Skip if metadata not found continue } entries = append(entries, entry) } return entries, nil } // Remove removes a job from the DLQ func (dlq *DeadLetterQueue) Remove(ctx context.Context, jobID string) error { // Remove from sorted set if err := dlq.client.ZRem(ctx, dlqKey, jobID).Err(); err != nil { return fmt.Errorf("failed to remove from DLQ: %w", err) } // Remove metadata metaKey := dlqMetaPrefix + jobID if err := dlq.client.Del(ctx, metaKey).Err(); err != nil { return fmt.Errorf("failed to remove DLQ metadata: %w", err) } return nil } // Count returns the total number of jobs in the DLQ func (dlq *DeadLetterQueue) Count(ctx context.Context) (int64, error) { count, err := dlq.client.ZCard(ctx, dlqKey).Result() if err != nil { return 0, fmt.Errorf("failed to count DLQ: %w", err) } return count, nil } // Retry removes a job from DLQ and returns it for retry func (dlq *DeadLetterQueue) Retry(ctx context.Context, jobID string) (*model.Job, error) { entry, err := dlq.Get(ctx, jobID) if err != nil { return nil, err } // Remove from DLQ if err := dlq.Remove(ctx, jobID); err != nil { return nil, err } return entry.Job, nil }