Documentation
¶
Overview ¶
Package sendramp implements durable, per-domain recipient-volume ramping for the asynchronous outbound delivery worker.
Index ¶
- Constants
- Variables
- func ConfirmTx(ctx context.Context, tx pgx.Tx, messageID string) error
- func Qualifies(confirmed, limit int) bool
- func ReleaseTx(ctx context.Context, tx pgx.Tx, messageID string) error
- type Decision
- type MaintenanceArgs
- type MaintenanceJobs
- type MaintenanceWorker
- type PermanentError
- type ReserveRequest
- type Schedule
- type ScopeState
- type Snapshot
- type Store
- func (s *Store) Confirm(ctx context.Context, messageID string) error
- func (s *Store) Exempt(ctx context.Context, userID, domain string) error
- func (s *Store) Release(ctx context.Context, messageID string) error
- func (s *Store) Reserve(ctx context.Context, req ReserveRequest) (Decision, error)
- func (s *Store) Resolve(ctx context.Context, messageID string) error
- func (s *Store) Snapshot(ctx context.Context, userID, domain string, now time.Time) (Snapshot, error)
- func (s *Store) Sweep(ctx context.Context, now time.Time) error
Constants ¶
const ( StatusInactive = "inactive" StatusRamping = "ramping" StatusComplete = "complete" StatusExempt = "exempt" )
const MinimumStartDaily = 50
Variables ¶
var DefaultSchedule = Schedule{StartDaily: 50, TargetDaily: 2000, RampDays: 30}
Functions ¶
func ConfirmTx ¶ added in v1.9.0
ConfirmTx records authoritative provider acceptance inside a caller's transaction, in the normative suborder.
Confirmation is the only thing that advances the ramp. That is deliberate: progress must measure delivered volume, not attempts, or a domain could age into full allowance by repeatedly failing.
func Qualifies ¶
Qualifies reports whether provider-accepted recipient volume reached half of the day's snapshotted allowance, rounded up.
func ReleaseTx ¶ added in v1.9.0
ReleaseTx returns a still-reserved message's ramp units inside a caller's transaction.
It skips the scope key, which it never writes. Skipping is permitted; what is not permitted is taking the reservation before a key that comes earlier, and this takes only the reservation and then the counter — a suffix of the normative order.
Types ¶
type Decision ¶
type Decision struct {
Allowed bool
Status string
DailyLimit int
UsedToday int
RetryAt time.Time
// IdentityUnverified reports the one refusal that is not about volume: the
// domain has no verified SENDING identity, so there is no proven scope for
// the ramp to charge and no daily allowance the caller could wait out. It
// rides the decision rather than an error because it is an ordinary answer
// about this send, and because the retry advice it deserves differs from a
// capacity hold's: what clears it is the customer finishing verification,
// not the next UTC midnight.
IdentityUnverified bool
}
type MaintenanceArgs ¶
type MaintenanceArgs struct{}
func (MaintenanceArgs) Kind ¶
func (MaintenanceArgs) Kind() string
type MaintenanceJobs ¶
type MaintenanceJobs struct {
// contains filtered or unexported fields
}
func NewMaintenanceJobs ¶
func NewMaintenanceJobs(store *Store) *MaintenanceJobs
func (*MaintenanceJobs) RegisterJobs ¶
func (m *MaintenanceJobs) RegisterJobs(w *river.Workers) []*river.PeriodicJob
type MaintenanceWorker ¶
type MaintenanceWorker struct {
river.WorkerDefaults[MaintenanceArgs]
// contains filtered or unexported fields
}
func NewMaintenanceWorker ¶
func NewMaintenanceWorker(store *Store) *MaintenanceWorker
func (*MaintenanceWorker) Work ¶
func (w *MaintenanceWorker) Work(ctx context.Context, _ *river.Job[MaintenanceArgs]) error
type PermanentError ¶
type PermanentError struct{ Err error }
func (*PermanentError) Error ¶
func (e *PermanentError) Error() string
func (*PermanentError) Permanent ¶
func (e *PermanentError) Permanent() bool
func (*PermanentError) Unwrap ¶
func (e *PermanentError) Unwrap() error
type ReserveRequest ¶
type Schedule ¶
Schedule is snapshotted when a domain first sends through its verified identity. Progress is measured in UTC days that reach the provider-accepted volume threshold, so idle or token sends cannot age into full volume.
func NewSchedule ¶
func (Schedule) CapForActiveDay ¶
CapForActiveDay returns the recipient allowance for a zero-based qualified-day index. The target is reached on the final configured ramp day.
type ScopeState ¶ added in v1.9.0
type ScopeState struct {
// Status is the domain's ramp status: inactive, ramping, complete, exempt.
Status string
// ActiveDays is how many UTC days reached the qualifying accepted volume.
ActiveDays int
// Established reports whether the scope has left probation.
Established bool
}
ScopeState is the ramp's answer to "has this domain proved itself yet".
func InspectScopeTx ¶ added in v1.9.0
InspectScopeTx classifies a domain without locking or writing anything.
The unlocked read is deliberate and safe in the only direction that matters. Ramp progress is monotonic — a scope goes inactive → ramping → qualified → complete and never regresses — so a stale read can only be stale in the STRICT direction, reporting probation for a scope that has just graduated. Charging the probation pool for one extra send is harmless; the reverse would not be, and cannot happen.
This matters because the probation classification decides which budget counters a transaction must lock, and the budget counters come BEFORE the ramp keys in the normative order. Something has to be read before the ramp lock is taken, and monotonicity is what makes that sound.
The sending identity below is the one input that is NOT monotonic — a domain can lose verification as well as gain it — so a stale read there could report established for an identity that has just gone unverified. That costs nothing this argument needs: ReserveTx re-reads the same column under the domain row lock and refuses the send outright, so the only consequence of the stale classification is which pool the refused attempt briefly charged.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) Exempt ¶
Exempt flips one verified, ramp-inactive domain to 'exempt'.
No automatic path calls this. The disabled ramp gate used to, once per eligible send, which made "this sender is established" a decision taken silently by the send path (see internal/agent.outboundRampGate.Reserve); hosted grandfathering is the audited one-shot in internal/sendingpolicy instead. This stays as the store-level primitive for an explicit, single-domain operator exemption and must not be re-wired to a hot path.
func (*Store) Reserve ¶
Reserve is the pool-owning wrapper. The logic lives in ReserveTx so the sending-protection gate can compose the ramp into its own transaction without a second implementation drifting from this one.
func (*Store) Resolve ¶
Resolve settles a pending reservation from the message's durable provider outcome. It is used by terminal reconciliation after ambiguous worker exits.