Help us shape the future of Go! Take the 2026 Go Developer Survey

sendlimit

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package sendlimit enforces an Integration's Send rate limit (ADR 0023): two token buckets, per-second and rolling 24 hours, whose state is one send_limiters row per Integration in Postgres (no Redis). A reservation takes one token from both buckets or from none, in a single UPDATE through the scoped client, so concurrent workers and instances cannot overspend; a denial reports how long until capacity returns.

Buckets are stored as a fill fraction of capacity (1 is full). Capacity and refill derive from the limits passed in on every call, so a user changing a limit takes effect at once, and a bucket with no limit is simply kept full.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RefreshQuota

func RefreshQuota(ctx context.Context, s *ent.Scoped, cipher *secrets.Cipher, catalog *messaging.Catalog, integ *ent.Integration) error

RefreshQuota asks the Integration's provider for its send quota and stores the answer, which EffectiveOf then folds into the ceiling by the minimum rule. It runs when the Integration is saved and hourly from the periodic job, because SES quotas grow as an account matures.

A failed lookup (no ses:GetSendQuota permission, an SES-compatible service such as Postbox, a timeout) never blocks sending and is not returned: it is recorded as the moment it first failed (provider_quota_failed_at), which the UI shows as a warning, and the last known provider values stay so a transient blip does not lift a ceiling. A later success clears the warning. Providers that cannot report a quota (SMTP) are left untouched. An error is returned only when the stored config cannot be read or the row cannot be written.

func Reserve

func Reserve(ctx context.Context, s *ent.Scoped, integrationID int64, l Limits, now time.Time) (time.Duration, error)

Reserve takes one token from both buckets at now. It returns a zero wait when the token was taken, or, when a bucket is short, the time until both buckets hold a token again (nothing is taken then). A call with no limit set does nothing.

func Spend

func Spend(ctx context.Context, s *ent.Scoped, integrationID int64, l Limits, now time.Time) error

Spend takes one token from both buckets at now without waiting and without the capacity check, so a bucket may go negative (Transactional, ADR 0023: never delayed, yet it makes later marketing sends wait longer). A call with no limit set does nothing.

func Statuses

func Statuses(ctx context.Context, s *ent.Scoped, rows []*ent.Integration, now time.Time) (map[int64]Status, error)

Statuses pairs the 24-hour usage with the effective ceilings of each Integration, keyed by Integration id. Usage is read once for all rows.

func Usage

func Usage(ctx context.Context, s *ent.Scoped, now time.Time) (map[int64]int, error)

Usage counts the messages each of the Workspace's Integrations sent over the 24 hours before now, keyed by Integration id (an Integration that sent nothing is absent). It counts every Outbound message the provider accepted, Transactional included, because that is what the provider's own quota sees.

Types

type Effective

type Effective struct {
	PerSecond Value
	PerDay    Value

	// ProviderQuotaUnavailable is set while the provider quota lookup fails (the
	// Integration's provider_quota_failed_at is set), so
	// the ceilings above rest on the manual value alone (or on none).
	ProviderQuotaUnavailable bool
}

Effective is an Integration's ceilings as enforced: per second and per rolling 24 hours. Per ADR 0023 each is the minimum of the available values; when two sources give the same number the user's manual value is reported.

func EffectiveOf

func EffectiveOf(integ *ent.Integration) Effective

EffectiveOf computes the ceilings an Integration is held to. It is the one place manual, provider (and later warmup) values are folded into a ceiling.

func (Effective) Limits

func (e Effective) Limits() Limits

Limits is what the token buckets enforce.

func (Effective) Unlimited

func (e Effective) Unlimited() bool

Unlimited reports whether no ceiling applies at all.

func (Effective) Warnings

func (e Effective) Warnings() []Warning

Warnings lists what is worth flagging about the ceilings, in a stable order; never nil.

type Limits

type Limits struct {
	PerSecond *int
	PerDay    *int
}

Limits are an Integration's configured ceilings; a nil field is not limited. The effective ceiling is the minimum of the available values (manual only for now), so this is where a provider-reported or warmup value would be folded in.

func (Limits) Any

func (l Limits) Any() bool

Any reports whether at least one ceiling is set.

func (Limits) Interval

func (l Limits) Interval() time.Duration

Interval is the spacing that keeps a stream of messages within the limits: one over the effective rate, which is the slower of the per-second ceiling and the 24-hour ceiling spread evenly over the day. It is zero when no ceiling is set.

type Source

type Source string

Source says where an effective ceiling comes from.

const (
	// SourceManual is the user-set value on the Integration.
	SourceManual Source = "manual"
	// SourceProvider is the value the provider reports (SES GetSendQuota).
	SourceProvider Source = "provider"
)

type Status

type Status struct {
	PerSecond Value
	PerDay    Value
	Sent24h   int
	Warnings  []Warning
}

Status is an Integration's Send rate limit as shown to a user: the ceilings as enforced, the messages sent over the last 24 hours and what is worth flagging. Each surface maps it to its own DTO.

A Value with a nil Limit has an empty Source (see Value), so no surface restates it.

type Value

type Value struct {
	Limit  *int
	Source Source
}

Value is one effective ceiling: Limit is nil (and Source empty) when the window is not limited.

type Warning

type Warning string

Warning names something a user should look at on an Integration's limit. The values are the API's warning enum, so each surface converts with a plain cast.

const (
	// WarningUnlimited: no ceiling applies, so sends are not paced.
	WarningUnlimited Warning = "unlimited"
	// WarningProviderQuotaUnavailable: the provider's quota could not be read.
	WarningProviderQuotaUnavailable Warning = "providerQuotaUnavailable"
)

Jump to

Keyboard shortcuts

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