Documentation
¶
Overview ¶
Package outbox is the retry vocabulary and the retry arithmetic that rig's two queues share.
Two of them, and only ever two: notify's delivery table and auth's mail outbox. Both had their own copy of everything here, and both said so in comments — "this is notify.Permanent's twin", "notify's nextAttemptAt, to the line". This is those comments becoming a compiler-checked fact.
What is here, and what is deliberately not ¶
Here: what a provider's answer means, when to come back, whether a pass has room for another send, and which leases this process is holding. Every one of those had no room to differ, and the two copies did not differ.
Not here: the claim-send-mark pass itself. notify groups a digest and marks N deliveries per message; auth reads three stores and rotates a token per row; and their release statements are scoped differently — notify releases the ids it tracked, auth releases everything it ever claimed. A shared pass would be one shape with two bodies and a parameter list longer than either, which is worse than two passes that each read straight through.
It is also why this can exist at all: a leaf over uuid, in `runtime`, which both service modules already depend on and neither depends on the other.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsPermanent ¶
IsPermanent reports whether err asks not to be retried.
It reads through wrapping, so an error a sender passed Permanent and then annotated with fmt.Errorf still answers true.
func Permanent ¶
Permanent wraps an error a retry cannot fix, so the row is marked Failed on this attempt rather than spending the rest of its budget.
The case it is for is a provider refusing the recipient rather than refusing the request: an address that does not exist, a device token the vendor has revoked, a message the provider will never accept. Fourteen attempts over eight hours at any of those is the same answer fourteen times.
Use it only when the provider's answer is about the recipient. A 500, a timeout, a connection refused and a 429 are all about the provider, and all of them are what the ordinary schedule is for — wrapping one of those here turns a provider's bad ten minutes into permanently undelivered mail.
Permanent(nil) is nil. That is not a courtesy: a pass decides a send succeeded by testing the error against nil, so a helper that manufactured a non-nil wrapper from a nil error would mark every successful send as a permanent failure. Any helper here that can be handed a nil says what it does with one.
func RetryAfter ¶
RetryAfter wraps an error with the earliest a retry is worth making, which is most often a 429 or a 503 carrying a Retry-After header.
The interval replaces the computed backoff for this attempt only — it does not change where the doubling had got to, so a provider that asks for ten minutes once does not reset the schedule. It is honoured as a floor rather than exactly, because a hundred rows carrying one provider's boundary cannot all return at that boundary without rebuilding the herd the spread exists to prevent.
It does not extend the attempt budget. A provider asking to be retried after longer than the remaining attempts can cover will still run out of them, and that is the intended reading of a maximum: a stop, not a negotiation.
RetryAfter(nil, d) is nil, for the reason Permanent gives. A d of zero or less is a plain wrap, so the ordinary backoff applies — a provider that sent a Retry-After of 0 is saying "now", and "now" for a queue is "next pass".
func RetryAfterOf ¶
RetryAfterOf is the interval err asks to be retried after, and whether it asked at all.
A permanent error is not retried at all, so this answers false for one even if RetryAfter was also applied: "do not retry" and "retry at this time" cannot both be honoured, and refusing to retry is the stronger of the two. Reading them in the other order would let a sender that wrapped both keep alive a row it had already said was hopeless.
func RoomFor ¶
RoomFor reports whether pass has room for one whole send.
The question is whether the *next* send fits, not whether the budget has already run out: a send started with a millisecond left runs to the pass deadline and no further, which is a call still in flight as the lease expires. So a pass ends with the send timeout unspent, and that is the point.
Types ¶
type Backoff ¶
type Backoff struct {
// Base is the first wait, and what every subsequent one doubles from.
Base time.Duration
// Cap bounds one wait, so a long schedule's tail is a series of knocks rather
// than a sleep nobody will be awake for.
Cap time.Duration
// Jitter is where the spread comes from: given n, something in [0, n).
//
// It exists to be replaced in a test and for nothing else. A nil one panics
// on the first failed send, which is a constructor that forgot to resolve it
// — and a silent zero spread would be the herd this exists to prevent.
Jitter func(int64) int64
}
Backoff is the retry ladder: a floor, a ceiling, and the spread that keeps a hundred rows from meeting one provider at the same instant.
A value rather than three parameters, because the three are one decision and a call site that passed two of them is a call site somebody could pass one of them to. Both durations must already be resolved — a zero Base schedules every retry for now — and the invariant that Cap is not below Base is the caller's to enforce, where it can still name which of the two numbers to change.
func (Backoff) Next ¶
Next is when to try again: the doubling, capped, spread.
The spread is added on top of the wait rather than taken out of it, so the nominal schedule stays a floor and Base keeps meaning what it says. A non-positive asked means the ladder; a positive one replaces this attempt's wait without moving where the doubling had got to, so a provider that asks for ten minutes once does not reset the schedule.
type Leases ¶
type Leases struct {
// contains filtered or unexported fields
}
Leases are the claims this process is holding, so a clean shutdown can give them back rather than leaving them to expire.
The lease TTL is for crashes. A process that knows it is going has no excuse for being slow about saying so, and leaving its claims turns every ordinary rollout into a delivery delay — repeatedly, for a rollout that replaces every pod.
Its own lock rather than the caller's, because the two things a dispatcher guards — whether it is still claiming, and what it is holding — are never read together, and sharing a mutex only made that harder to see.
Safe for concurrent use. The zero value is usable.
func (*Leases) Clear ¶
func (l *Leases) Clear()
Clear forgets everything.
Leases.Drop is what a pass giving back its own claims should reach for: two passes can run at once — an in-process goroutine and a cron task both calling the same dispatch — and clearing there would wipe the other one's claims, leaving them to expire on a TTL meant for crashes.