150 lines
4.0 KiB
Go
150 lines
4.0 KiB
Go
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
|
|
}
|