package queue import ( "hash/fnv" "math" "time" ) type RetryAction string const ( RetryActionRetry RetryAction = "retry" RetryActionDeadLetter RetryAction = "dead_letter" RetryActionAck RetryAction = "ack" ) type RetryDecision struct { Action RetryAction Delay time.Duration Reason string } type RetryPolicy interface { Decide(delivery *Delivery, envelope *Envelope, err error) RetryDecision } type ExponentialBackoffPolicy struct { minDelay time.Duration maxDelay time.Duration permanentFailureDelay time.Duration } func NewExponentialBackoffPolicy(minDelay, maxDelay, permanentFailureDelay time.Duration) *ExponentialBackoffPolicy { if minDelay <= 0 { minDelay = time.Second } if maxDelay < minDelay { maxDelay = minDelay } if permanentFailureDelay <= 0 { permanentFailureDelay = 30 * time.Second } return &ExponentialBackoffPolicy{ minDelay: minDelay, maxDelay: maxDelay, permanentFailureDelay: permanentFailureDelay, } } func (p *ExponentialBackoffPolicy) Decide(delivery *Delivery, _ *Envelope, err error) RetryDecision { if err == nil { return RetryDecision{Action: RetryActionAck} } if IsPermanent(err) { return RetryDecision{ Action: RetryActionDeadLetter, Delay: p.permanentFailureDelay, Reason: err.Error(), } } attempt := 1 if delivery != nil && delivery.ReceiveCount > 0 { attempt = delivery.ReceiveCount } delay := p.backoffWithJitter(deliveryID(delivery), attempt) return RetryDecision{ Action: RetryActionRetry, Delay: delay, Reason: err.Error(), } } func (p *ExponentialBackoffPolicy) backoffWithJitter(seed string, attempt int) time.Duration { if attempt < 1 { attempt = 1 } power := math.Pow(2, float64(attempt-1)) delay := time.Duration(float64(p.minDelay) * power) if delay > p.maxDelay { delay = p.maxDelay } if delay <= 0 { return p.minDelay } jitter := deterministicJitter(seed, delay/2) return delay/2 + jitter } func deterministicJitter(seed string, max time.Duration) time.Duration { if max <= 0 { return 0 } h := fnv.New64a() _, _ = h.Write([]byte(seed)) return time.Duration(h.Sum64() % uint64(max)) } func deliveryID(delivery *Delivery) string { if delivery == nil { return "unknown" } if delivery.ID != "" { return delivery.ID } return "unknown" }