Module quotas

Module quotas 

Source
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§

BudgetUsage
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 quotas for consumer row consumer_id (DW-033). Pure policy over StateStore::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).