Add initial documentation
This commit is contained in:
186
docs/30_QUOTA_ENFORCEMENT_FLOW1.md
Normal file
186
docs/30_QUOTA_ENFORCEMENT_FLOW1.md
Normal file
@ -0,0 +1,186 @@
|
||||
# 26 · Quota‑Enforcement Flow (333 Scans/Day)
|
||||
|
||||
> **Audience:** backend contributors, UI devs, CI‑plugin authors, third‑party
|
||||
> tool builders
|
||||
> **Version:** 1.0 (18 Jul 2025) — aligns with Quota rev 2.0
|
||||
> **Scope:** runtime data‑flow, key formats, timing budget, error model
|
||||
|
||||
---
|
||||
|
||||
## 0 Why another doc?
|
||||
|
||||
* **SRS** lists the *requirements* (FR‑4, FR‑4a…).
|
||||
* **Module Specs** describe the *code surface* (`IQuotaService`, `QuotaVerdict`).
|
||||
* **This file** stitches everything together so future maintainers can reason
|
||||
about race conditions, UX timing and API behaviour in one place.
|
||||
|
||||
---
|
||||
|
||||
## 1 High‑Level Sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as SanTech CLI
|
||||
participant API as API Gateway
|
||||
participant QU as Quota Service
|
||||
participant RD as Redis
|
||||
participant SCN as Scan Runner
|
||||
|
||||
CLI->>API: POST /scan
|
||||
API->>QU: CheckAsync(token)
|
||||
QU->>RD: LUA INCR quota:<token>
|
||||
alt Remaining > 0
|
||||
RD-->>QU: counter + reset ts
|
||||
QU-->>API: verdict {IsAllowed=true}
|
||||
API->>SCN: enqueue scan
|
||||
SCN-->>API: result
|
||||
API-->>CLI: 200 OK (headers: remaining, reset)
|
||||
else Remaining == 0 → soft wall
|
||||
RD-->>QU: counter==limit
|
||||
QU-->>API: verdict {RetryAfter=5 s}
|
||||
API-->>CLI: 429 Too Many (Retry‑After: 5)
|
||||
else Hard Wall (≥ limit+30)
|
||||
RD-->>QU: counter≥limit+30
|
||||
QU-->>API: verdict {RetryAfter=60 s}
|
||||
API-->>CLI: 429 Too Many (Retry‑After: 60)
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
* **Soft wait‑wall** applies to the *first 30* blocked scans.
|
||||
* **Hard wait‑wall** (60 s) kicks in afterwards until the counter resets at
|
||||
**00:00 UTC**.
|
||||
|
||||
---
|
||||
|
||||
## 2 Key Redis Structures
|
||||
|
||||
| Key pattern | Type | TTL | Purpose |
|
||||
|-------------|------|-----|---------|
|
||||
| `quota:<token>:<yyyy‑mm‑dd>` | *string* | *expires at UTC midnight* | Counter starting at `1` |
|
||||
|
||||
### Typical value progression
|
||||
|
||||
```json
|
||||
{
|
||||
"quota:abc123:2025-07-18": "198",
|
||||
"quota:abc123:2025-07-18-RESET": "2025-07-19T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3 QuotaVerdict DTO
|
||||
|
||||
```csharp
|
||||
public readonly record struct QuotaVerdict(
|
||||
bool IsAllowed,
|
||||
int Remaining,
|
||||
DateTimeOffset ResetUtc,
|
||||
TimeSpan RetryAfter)
|
||||
{
|
||||
public static QuotaVerdict Allowed(int remaining, DateTimeOffset resetUtc)
|
||||
=> new(true, remaining, resetUtc, TimeSpan.Zero);
|
||||
|
||||
public static QuotaVerdict Blocked(int remaining, DateTimeOffset resetUtc,
|
||||
TimeSpan retryAfter)
|
||||
=> new(false, remaining, resetUtc, retryAfter);
|
||||
}
|
||||
```
|
||||
|
||||
* **IsAllowed** `false` ⇒ API must return `429`
|
||||
* **RetryAfter** `> 0` ⇒ populate `Retry‑After` header (seconds)
|
||||
|
||||
---
|
||||
|
||||
## 4 HTTP Headers & Error Model
|
||||
|
||||
| Header | Example | Meaning |
|
||||
|--------|---------|---------|
|
||||
| `X‑Stella‑Quota‑Remaining` | `117` | Scans left for the token today |
|
||||
| `X‑Stella‑Reset` | `2025‑07‑18T23:59:59Z` | When the counter resets |
|
||||
| `Retry‑After` | `5` or `60` | Seconds the client should back‑off |
|
||||
|
||||
Error body (RFC 7807):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://stella-ops.org/probs/quota",
|
||||
"title": "Daily quota exceeded",
|
||||
"status": 429,
|
||||
"detail": "This token exhausted its 333 scans for 2025‑07‑18.",
|
||||
"quota": {
|
||||
"remaining": 0,
|
||||
"reset": "2025‑07‑18T23:59:59Z"
|
||||
},
|
||||
"traceId": "00-1db39d4f3ab16fa1"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5 UI Behaviour
|
||||
|
||||
| Threshold | Visual cue |
|
||||
|-----------|------------|
|
||||
| **≤ 133 scans remaining** | Yellow banner “*60 % of free tier used*” |
|
||||
| **0 remaining** (soft) | Red toast “*Back‑off 5 s*”; spinner shows countdown |
|
||||
| **0 remaining** (hard) | Modal blocks *New Scan* button; shows **HH:MM:SS** until reset |
|
||||
|
||||
*WebSocket `quota/events` pushes live counters to avoid polling.*
|
||||
|
||||
---
|
||||
|
||||
## 6 Timing Budget
|
||||
|
||||
| Operation | Budget (P95) | Impl detail |
|
||||
|-----------|--------------|-------------|
|
||||
| Redis LUA INCR | ≤ 0.8 ms | SHA script cached |
|
||||
| Verdict calc | ≤ 0.2 ms | branchless math |
|
||||
| Header injection | ≤ 0.1 ms | middleware sets `OnStarting` callback |
|
||||
| Soft delay | **exactly 5 000 ms** ± 50 ms | `Task.Delay` in controller |
|
||||
| Hard delay | **exactly 60 000 ms** ± 100 ms | same |
|
||||
|
||||
---
|
||||
|
||||
## 7 CLI Auto‑Retry Logic (SanTech v0.8+)
|
||||
|
||||
```csharp
|
||||
var resp = await http.PostAsync("/scan", payload, ct);
|
||||
if (resp.StatusCode == (HttpStatusCode)429 &&
|
||||
autoRetry && retryCount < 1)
|
||||
{
|
||||
var after = int.Parse(resp.Headers.RetryAfter.Delta!.Value.TotalSeconds);
|
||||
Console.Error.WriteLine($"Quota hit – waiting {after}s…");
|
||||
await Task.Delay(TimeSpan.FromSeconds(after), ct);
|
||||
retryCount++;
|
||||
goto Retry;
|
||||
}
|
||||
```
|
||||
|
||||
*Retrieves `Retry‑After`; retries **once** if ≤ 60 s.*
|
||||
|
||||
---
|
||||
|
||||
## 8 Edge Cases & Tests
|
||||
|
||||
| Case | Expected behaviour | Covered by |
|
||||
|------|--------------------|-----------|
|
||||
| Redis restart (counter key lost) | API treats missing key as `0`; safe default | IT‑QUOTA‑REDIS |
|
||||
| Two parallel scans exceed limit simultaneously | Only one passes; second gets `429` | RaceCondTests.cs |
|
||||
| Token with custom limit = 100 | Soft wall at ≥ 100; headers reflect custom limit | QuotaCustomLimitTests.cs |
|
||||
|
||||
---
|
||||
|
||||
## 9 Migration Notes (v0.7 → v0.8)
|
||||
|
||||
* Old key `quota:<token>` (no date) **deprecated** — migration script copies
|
||||
value to the new *dated* key and sets TTL to next UTC midnight.
|
||||
|
||||
---
|
||||
|
||||
## 10 Change Log
|
||||
|
||||
| Ver | Date | Note |
|
||||
|-----|------|------|
|
||||
| 1.0 | 18 Jul 2025 | Initial end‑to‑end flow doc (Quota rev 2.0). |
|
Reference in New Issue
Block a user