sendramp

package
v1.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package sendramp implements durable, per-domain recipient-volume ramping for the asynchronous outbound delivery worker.

Index

Constants

View Source
const (
	StatusInactive = "inactive"
	StatusRamping  = "ramping"
	StatusComplete = "complete"
	StatusExempt   = "exempt"
)
View Source
const MinimumStartDaily = 50

Variables

View Source
var DefaultSchedule = Schedule{StartDaily: 50, TargetDaily: 2000, RampDays: 30}

Functions

func ConfirmTx added in v1.9.0

func ConfirmTx(ctx context.Context, tx pgx.Tx, messageID string) error

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

func Qualifies(confirmed, limit int) bool

Qualifies reports whether provider-accepted recipient volume reached half of the day's snapshotted allowance, rounded up.

func ReleaseTx added in v1.9.0

func ReleaseTx(ctx context.Context, tx pgx.Tx, messageID string) error

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
}

func ReserveTx added in v1.9.0

func ReserveTx(ctx context.Context, tx pgx.Tx, req ReserveRequest) (Decision, error)

ReserveTx acquires ramp capacity for one message inside a caller's transaction, in the normative suborder: domain identity, registrable-domain scope, message reservation, UTC day counter.

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

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 ReserveRequest struct {
	MessageID string
	UserID    string
	Domain    string
	Units     int
	Day       time.Time
	Schedule  Schedule
}

type Schedule

type Schedule struct {
	StartDaily  int
	TargetDaily int
	RampDays    int
}

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 NewSchedule(startDaily, targetDaily, rampDays int) Schedule

func (Schedule) CapForActiveDay

func (s Schedule) CapForActiveDay(activeDay int) int

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

func InspectScopeTx(ctx context.Context, tx pgx.Tx, userID, domain string) (ScopeState, error)

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 Snapshot

type Snapshot struct {
	Status      string
	StartedAt   *time.Time
	CompletedAt *time.Time
	ActiveDays  int
	StartDaily  int
	TargetDaily int
	RampDays    int
	DailyLimit  int
	UsedToday   int
}

type Store

type Store struct {
	// contains filtered or unexported fields
}

func NewStore

func NewStore(pool *pgxpool.Pool) *Store

func (*Store) Confirm

func (s *Store) Confirm(ctx context.Context, messageID string) error

func (*Store) Exempt

func (s *Store) Exempt(ctx context.Context, userID, domain string) error

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) Release

func (s *Store) Release(ctx context.Context, messageID string) error

func (*Store) Reserve

func (s *Store) Reserve(ctx context.Context, req ReserveRequest) (Decision, error)

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

func (s *Store) Resolve(ctx context.Context, messageID string) error

Resolve settles a pending reservation from the message's durable provider outcome. It is used by terminal reconciliation after ambiguous worker exits.

func (*Store) Snapshot

func (s *Store) Snapshot(ctx context.Context, userID, domain string, now time.Time) (Snapshot, error)

func (*Store) Sweep

func (s *Store) Sweep(ctx context.Context, now time.Time) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL