Expand description
Consumer request budgets (DW-033): daily/monthly quotas and metering.
§What a quota is (and is not)
A quota is a BUDGET, not a rate: crate::config::ConsumerQuotas
caps the total number of requests one consumer may make inside a
fixed UTC calendar window — a day (midnight to midnight UTC) or a
month (the first through the last instant of the UTC month). The
rate limiter (DW-017) shapes traffic inside seconds or minutes via
GCRA replenishment; a quota never replenishes inside its window,
just resets whole at the boundary. The two mechanisms are SEPARATE
by design and compose: both apply when both are configured, and
quota 429s carry the same header family as rate-limit 429s
(Retry-After + X-RateLimit-Limit / -Remaining / -Reset)
because the client-facing contract is identical — “you may retry at
this time”.
§Counters and durability
Usage counters are the state store’s quota_counters rows (DW-018
seeded the table and StateStore::incr_quota’s atomic
increment-or-refuse — this module is the policy layer that was
missing). Counters therefore persist restarts and crashes like the
rest of the store: every accepted request’s increment is a
synchronous point write committed before the request proceeds, not
a fire-and-forget record, so a crash can lose at most the request
that was in flight — never a committed counter. This is the OSS
local-counter implementation the issue scopes: per-instance SQLite
(or in-memory) truth. A fleet-wide shared-counter variant (Redis)
is the Ent follow-up (DW-155) and deliberately does not exist here.
§Hot-path cost (documented trade)
Every request of a quota-configured consumer performs one or two
SYNCHRONOUS SQLite write transactions (incr_quota) on the single
mutex-guarded state-store connection — and that store keeps
SQLite’s default synchronous=FULL (an fsync per commit; the
analytics store pins NORMAL for contrast, but relaxing the state
store’s durability is a separate decision touching credential-write
semantics, not this feature’s call). Budgets therefore add a
per-request fsync and serialize quota’d traffic through one
connection — the accepted OSS per-instance shape; scaling quota
enforcement across a fleet is exactly the DW-155 follow-up.
§Window math
Windows are UTC calendar aligned and computed purely from an
epoch-seconds input (day_window, month_window) so tests are
deterministic. The day window is days * 86400; the month window
uses the standard civil-calendar days algorithm (no calendar
dependency). reset_epoch_s is the boundary at which the budget
becomes whole again — the value advertised in X-RateLimit-Reset.
§Evaluation semantics (mirrors DW-017 where the client sees it)
Budgets evaluate shortest-window-first (daily, then monthly). All
configured budgets apply (AND). A denied request answers 429 with
Retry-After = seconds until the denying budget’s window resets
(rounded up, minimum 1) and the FIRST denying budget’s
Limit/Remaining/Reset headers; when a LATER budget is ALSO
exhausted, Retry-After stretches to that later wall (the monthly
boundary is a midnight at or after the daily one) — a client
honoring the hint never retries out of the daily wall straight into
the monthly one, the same max-wait rule the multi-rule rate limiter
applies. Evaluation is decide-and-reserve and STOPS at the first
denial (the GCRA stacking rule): a request denied by the monthly
budget has already spent its daily unit (the daily window evaluated
first), while a request denied by the daily budget consumes
NOTHING — later budgets are peeked read-only for the max-wait
computation, never incremented for a refused request. The stacking
waste is one unit in the faster-resetting window; never more
permissive, bounded, documented.
§Failure model
The store’s failure answers are surfaced, not hidden:
QuotaOutcome::Unavailable (a SQLite error) maps to a 500 on the
request path — the gateway cannot vouch for the budget either way,
the same posture as an authN backend failure. A consumer present in
config but absent from the store (sync never ran) fails OPEN with a
logged warn: there is no counter to enforce, and 500-ing every
request of an unsynced consumer would turn a wiring gap into an
outage. Quota config without ANY attached state store is likewise
inert (warned once at request time from the dataplane — validation
cannot see DWARA_STATE_DB, which is runtime environment, not
config).
§Metering
Usage is queryable three ways: the admin API’s GET /quotas/usage
(current windows, per consumer), the dwara_quota_used /
dwara_quota_limit / dwara_quota_denied_total metric families
(DW-021 conventions), and the analytics store — a quota-denied
request completes with rate_limited = true and its consumer name,
so the existing per-consumer analytics axis (raw rows and the
rollup_fixed consumer dimension) already answers “how much did
this consumer send and how much was refused” over history.
Re-exports§
pub use crate::config::quotas::day_window;pub use crate::config::quotas::month_window;pub use crate::config::quotas::retry_after;pub use crate::config::quotas::Budget;pub use crate::config::quotas::QuotaOutcome;pub use crate::config::quotas::DAILY_KEY;pub use crate::config::quotas::MONTHLY_KEY;
Structs§
- Budget
Usage - One budget’s current-window usage figure (the admin API and metric gauges’ read model).
Functions§
- check
- Check-and-reserve one request against every configured budget of
quotasfor consumer rowconsumer_id(DW-033). Pure policy overStateStore::incr_quota; see the module docs for the evaluation order, the stacking consumption trade, and the failure model. - current_
usage - Read the current-window usage of every configured budget for one consumer (no reservation, no increment — the query side of metering). A store that cannot answer yields an empty read; callers surface the gap their own way (the admin endpoint 500s, the gauge refresh skips).