feat: add production capability foundations
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Redis Distributed Rate-Limit Increment Design
|
||||
|
||||
**Status:** implemented as standalone R1
|
||||
|
||||
**Parent:** `2026-07-26-redis-production-capability-design.md` §§19–21
|
||||
|
||||
## Goal and readiness
|
||||
|
||||
Provide three selectable, bounded distributed rate-limit algorithms:
|
||||
|
||||
- fixed window;
|
||||
- sliding-window counter;
|
||||
- token bucket.
|
||||
|
||||
This increment is a standalone Redis R1 provider. It does not claim R2 topology/security/failover
|
||||
qualification and does not implement sliding log, GCRA, leaky bucket, evaluation dedup, hierarchical
|
||||
all-or-nothing policies or local emergency fallback.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `shared-contract` owns the edge-enforcement semantic port and provider-neutral request, policy,
|
||||
decision and failure outcomes. Business quotas remain application use-case policy and do not use
|
||||
this port.
|
||||
- `adapter:outbound:cache-redis` owns Redis keys, atomic Lua programs, structured reply parsing,
|
||||
failure certainty and the provider implementation.
|
||||
- `app-bootstrap` owns the explicit provider/policy selection.
|
||||
- The existing inbound-web local limiter remains a compatibility path until a separate inbound
|
||||
migration. Its types do not cross into the Redis provider.
|
||||
|
||||
The rate-limit runtime does not reuse `app.cache.redis`, the cache connection or cache fail-open
|
||||
decorators. Coordination has different failure and deployment semantics.
|
||||
|
||||
## Shared semantic contract
|
||||
|
||||
`EdgeRateLimitPort.evaluate(RateLimitRequest)` accepts:
|
||||
|
||||
- bounded `policyId`;
|
||||
- already pseudonymized/bounded `subjectDigest`;
|
||||
- positive request cost;
|
||||
- optional evaluation ID (rejected in this non-deduplicating revision);
|
||||
- finite caller deadline.
|
||||
|
||||
`RateLimitPolicy` freezes policy ID/revision, one algorithm-specific parameter subtype, maximum
|
||||
cost, cleanup grace, maximum clock regression and `FAIL_CLOSED`. Construction rejects mismatched
|
||||
algorithm/parameters, arithmetic outside Lua's exact integer range and unsupported failure/dedup
|
||||
claims.
|
||||
|
||||
The outcome is one of:
|
||||
|
||||
- `Evaluated(decision)`;
|
||||
- `Unavailable(policyId, retryAfter, category)` for known pre-send/no-mutation failures and unsafe
|
||||
server clock;
|
||||
- `Indeterminate(policyId, retryAfter)` for post-dispatch uncertain mutation;
|
||||
- `Incompatible(policyId, category)` for state/program/reply mismatch.
|
||||
|
||||
`RateLimitDecision` includes allow/deny, limit, remaining, retry-after, reset-at, policy ID/revision,
|
||||
`GLOBAL_REDIS` source and certainty. Fixed window and token bucket are `CERTAIN`;
|
||||
sliding-window counter is `APPROXIMATE_ALGORITHM`.
|
||||
|
||||
## Atomic programs
|
||||
|
||||
Each v1 program uses one versioned hash key and calls Redis `TIME` exactly once.
|
||||
|
||||
```text
|
||||
rate-fixed-window-v1.lua
|
||||
rate-sliding-counter-v1.lua
|
||||
rate-token-bucket-v1.lua
|
||||
```
|
||||
|
||||
Every program returns exactly seven bounded scalar fields:
|
||||
|
||||
```text
|
||||
status, serverNowMillis, effectiveNowMillis,
|
||||
limit, remaining, retryAfterMillis, resetAtMillis
|
||||
```
|
||||
|
||||
Statuses are `ALLOWED`, `DENIED`, `CLOCK_UNSAFE`, `STATE_INCOMPATIBLE`, `INVALID`.
|
||||
Unknown arity/status/numeric syntax/range is a compatibility failure, never allow/fail-open.
|
||||
|
||||
Common rules:
|
||||
|
||||
- Redis server time drives enforcement;
|
||||
- small backward movement clamps to stored `lastObservedMillis`;
|
||||
- regression beyond policy threshold returns `CLOCK_UNSAFE` without consuming state;
|
||||
- policy/schema/algorithm mismatch returns `STATE_INCOMPATIBLE`;
|
||||
- denied requests do not consume quota;
|
||||
- state receives a finite TTL;
|
||||
- all arithmetic stays within `2^53-1`;
|
||||
- raw principal/IP/API-key/route never appears in the physical key.
|
||||
|
||||
The existing scalar Lua executor stays intact. A structured program path adds bounded MULTI reply
|
||||
support and uses `EVALSHA`, falling back to the exact compiled source only on `NOSCRIPT`.
|
||||
|
||||
## Algorithm rules
|
||||
|
||||
Fixed window stores window ID and consumed count. Allow increments only when
|
||||
`consumed + cost <= limit`; retry/reset points to the current window end.
|
||||
|
||||
Sliding counter stores previous/current window IDs and counts, using scale `1_000_000` and
|
||||
conservative ceiling weight. It reports approximate certainty and a bounded conservative retry.
|
||||
|
||||
Token bucket stores scaled tokens, last refill time and the sub-token division remainder. Refill is
|
||||
therefore independent of evaluation frequency, uses quotient/remainder arithmetic without an
|
||||
unsafe `numerator + denominator - 1` intermediate, and saturates at capacity. Denial does not
|
||||
subtract tokens; retry and full-reset use integer ceiling.
|
||||
|
||||
## Physical key
|
||||
|
||||
The existing canonical builder is reused with:
|
||||
|
||||
```text
|
||||
capability=rate
|
||||
region=<policyId>
|
||||
kind=state
|
||||
digest(policyId, policyRevision, algorithm, subjectDigest)
|
||||
```
|
||||
|
||||
Policy revision appears in both digest input and stored state. A policy revision therefore rolls to
|
||||
a new key while old state expires naturally.
|
||||
|
||||
## Runtime and composition
|
||||
|
||||
`app.rate-limit` is disabled by default. Enabling requires:
|
||||
|
||||
- `provider=redis`;
|
||||
- one default policy and an exact policy definition;
|
||||
- a dedicated Redis coordination endpoint and HMAC secret;
|
||||
- finite command/admission bounds.
|
||||
|
||||
Only `role=coordination` and `failure-policy=fail-closed` are accepted in v1. Disabled mode creates
|
||||
no connection, thread or semantic port. Cache Redis settings/beans are never an implicit fallback.
|
||||
|
||||
## Evidence
|
||||
|
||||
Unit tests cover contract bounds, policy arithmetic, key privacy/revision, structured reply
|
||||
validation, `NOSCRIPT`, boundary vectors, denial-no-consume, clock regression, pre/post-dispatch
|
||||
failure certainty and disabled composition. The explicit Redis 7.4 service lane executes all three
|
||||
programs, exact-boundary admission after a denied non-consuming request, excessive clock-regression
|
||||
state immutability, `TYPE` response normalization, token refill-remainder carry, malformed hash-state
|
||||
classification, cache `NX`, and observation-token compare-and-replace. Redis 7.4 is the minimum
|
||||
version declared by the program manifests until a lower-version service lane exists. The caller
|
||||
deadline is an admission precheck against the fixed command timeout; R1 does not claim per-command
|
||||
dynamic timeout or hard cancellation after dispatch. Missing TLS/ACL, Sentinel/Cluster, failover and
|
||||
persistence/eviction evidence keeps the provider at R1.
|
||||
Reference in New Issue
Block a user