88 lines
2.4 KiB
Go
88 lines
2.4 KiB
Go
package entity
|
|
|
|
import (
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type OutboundJobStatus string
|
|
|
|
const (
|
|
OutboundJobStatusQueued OutboundJobStatus = "QUEUED"
|
|
OutboundJobStatusSending OutboundJobStatus = "SENDING"
|
|
OutboundJobStatusDeferred OutboundJobStatus = "DEFERRED"
|
|
OutboundJobStatusDelivered OutboundJobStatus = "DELIVERED"
|
|
OutboundJobStatusBounced OutboundJobStatus = "BOUNCED"
|
|
OutboundJobStatusCancelled OutboundJobStatus = "CANCELLED"
|
|
)
|
|
|
|
type OutboundJob struct {
|
|
ID uuid.UUID
|
|
MailboxID *uuid.UUID
|
|
BlobID uuid.UUID
|
|
EnvelopeFrom string
|
|
EnvelopeTo string
|
|
DestinationDomain string
|
|
Status OutboundJobStatus
|
|
Attempt int
|
|
NextAttemptAt time.Time
|
|
ExpiresAt time.Time
|
|
LastSmtpCode string
|
|
LastError string
|
|
TlsPolicyUsed string
|
|
DelayDsnSent bool
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// CalculateNextBackoff computes the next retry time and DSN requirements from
|
|
// the attempt that has just failed. The delays follow PLAN.md section 6.3,
|
|
// which tabulates the wait *before* each attempt: attempt 2 runs 5 min after
|
|
// attempt 1 failed, attempt 4 an hour later and also emits the delay DSN, and
|
|
// from attempt 5 on the wait carries +/-20% jitter to avoid a thundering herd.
|
|
func CalculateNextBackoff(now time.Time, failedAttempt int, expiresAt time.Time) (nextAttemptAt time.Time, sendDelayDsn bool, permanentFailure bool) {
|
|
if !expiresAt.IsZero() && (now.After(expiresAt) || now.Equal(expiresAt)) {
|
|
return now, false, true
|
|
}
|
|
|
|
// The delay to apply is the one the retry table associates with the
|
|
// attempt about to be scheduled, not with the one that just failed.
|
|
nextAttempt := failedAttempt + 1
|
|
|
|
var baseDelay time.Duration
|
|
|
|
switch nextAttempt {
|
|
case 1:
|
|
baseDelay = 0
|
|
case 2:
|
|
baseDelay = 5 * time.Minute
|
|
case 3:
|
|
baseDelay = 15 * time.Minute
|
|
case 4:
|
|
baseDelay = 1 * time.Hour
|
|
sendDelayDsn = true
|
|
case 5:
|
|
baseDelay = 2 * time.Hour
|
|
case 6:
|
|
baseDelay = 4 * time.Hour
|
|
case 7:
|
|
baseDelay = 8 * time.Hour
|
|
default:
|
|
baseDelay = 12 * time.Hour
|
|
}
|
|
|
|
if baseDelay > 0 && nextAttempt >= 5 {
|
|
// Apply +/-20% jitter
|
|
jitterFactor := 0.8 + rand.Float64()*0.4
|
|
baseDelay = time.Duration(float64(baseDelay) * jitterFactor)
|
|
}
|
|
|
|
nextAttemptAt = now.Add(baseDelay)
|
|
if !expiresAt.IsZero() && nextAttemptAt.After(expiresAt) {
|
|
return expiresAt, sendDelayDsn, true
|
|
}
|
|
|
|
return nextAttemptAt, sendDelayDsn, false
|
|
}
|