internal/formation/notifications.go
internal/formation · 119 lines · 8 declarations · source
Declarations
type Notifications
type Notifications struct {
store NotificationStore
sender *notify.Sender
budget int
baseWait time.Duration
}
Notifications is the pass that tells a project its memory moved, and the loop that keeps trying.
Why it is a pass beside formation rather than part of it
Formation's job is to turn stored turns into memory, and it must not be able to fail because somebody's webhook is down. So this reads the watermark formation left behind, writes down what is owed, and sends on its own schedule. The worst a broken destination can do is fill a queue that an operator can read.
type NotificationStore
type NotificationStore interface {
Owed(ctx context.Context, scope string, formed, stored int64, parked int) (int, error)
Claim(ctx context.Context) (pg.Due, bool, error)
Delivered(ctx context.Context, id string, status int) error
Failed(ctx context.Context, id string, attempts, budget, status int, reason string, backoff time.Duration) error
}
NotificationStore is what this pass needs of the store, and no more. Named here rather than taken as a concrete type so the pass can be exercised against a store that fails on purpose.
const DefaultAttemptBudget
const DefaultAttemptBudget = 6
DefaultAttemptBudget is how many times a delivery is tried before it is parked.
Six, with doubling from five seconds, is about five minutes of trying. Longer would keep a queue full of news that has stopped being news — the receiver reads the watermark and catches up in one call — and shorter would park a destination that restarted.
func NewNotifications
func NewNotifications(store NotificationStore, sender *notify.Sender) *Notifications
NewNotifications builds the pass.
method Notifications.WithBudget
func (n *Notifications) WithBudget(budget int, baseWait time.Duration) *Notifications
WithBudget sets the attempt budget and the first wait, for a test that cannot spend five minutes.
method Notifications.Owe
func (n *Notifications) Owe(ctx context.Context, scope string, formed, stored int64, parked int) (int, error)
Owe records what a scope's watermark says is worth telling. Cheap and idempotent: the row it would write is usually already there, because most passes have no news.
method Notifications.Deliver
func (n *Notifications) Deliver(ctx context.Context, limit int) (delivered int, attempted int, err error)
Deliver attempts up to `limit` due deliveries and reports how many landed.
Bounded per pass because this shares a worker with formation, and a queue of a thousand failing destinations must not be able to stop memory being formed.
method Notifications.wait
func (n *Notifications) wait(attempts int) time.Duration
wait doubles with each attempt, because the failure that matters most is the one that is not this destination's fault: a receiver restarting fails every delivery at once, and a flat retry would spend the whole queue's budget inside a minute.