Files
git.stella-ops.org/docs/30_QUOTA_ENFORCEMENT_FLOW1.md

187 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#26 · QuotaEnforcement Flow (333 Scans/Day)
> **Audience:** backend contributors, UI devs, CIplugin authors, thirdparty
> tool builders
> **Version:** 1.0 (18 Jul 2025) — aligns with Quotarev 2.0
> **Scope:** runtime dataflow, key formats, timing budget, error model
---
## 0 Why another doc?
* **SRS** lists the *requirements* (FR4, FR4a…).
* **ModuleSpecs** 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 HighLevel 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 (RetryAfter: 5)
else HardWall (≥limit+30)
RD-->>QU: counter≥limit+30
QU-->>API: verdict {RetryAfter=60 s}
API-->>CLI: 429 Too Many (RetryAfter: 60)
end
```
* **Soft waitwall** applies to the *first 30* blocked scans.
* **Hard waitwall** (60s) kicks in afterwards until the counter resets at
**00:00UTC**.
---
## 2 Key Redis Structures
| Key pattern | Type | TTL | Purpose |
|-------------|------|-----|---------|
| `quota:<token>:<yyyymmdd>` | *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 `RetryAfter` header (seconds)
---
## 4 HTTP Headers & Error Model
| Header | Example | Meaning |
|--------|---------|---------|
| `XStellaQuotaRemaining` | `117` | Scans left for the token today |
| `XStellaReset` | `20250718T23:59:59Z` | When the counter resets |
| `RetryAfter` | `5` or `60` | Seconds the client should backoff |
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 20250718.",
"quota": {
"remaining": 0,
"reset": "20250718T23: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 “*Backoff 5s*”; 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 |
|-----------|--------------|-------------|
| RedisLUA INCR | ≤0.8ms | SHA script cached |
| Verdict calc | ≤0.2ms | branchless math |
| Header injection | ≤0.1ms | middleware sets `OnStarting` callback |
| Soft delay | **exactly 5000ms** ±50ms | `Task.Delay` in controller |
| Hard delay | **exactly 60000ms** ±100ms | same |
---
## 7 CLI AutoRetry 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 `RetryAfter`; retries **once** if ≤60s.*
---
## 8 Edge Cases & Tests
| Case | Expected behaviour | Covered by |
|------|--------------------|-----------|
| Redis restart (counter key lost) | API treats missing key as `0`; safe default | ITQUOTAREDIS |
| Two parallel scans exceed limit simultaneously | Only one passes; second gets `429` | RaceCondTests.cs |
| Token with custom limit =100 | Soft wall at100; 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 | 18Jul2025 | Initial endtoend flow doc (Quota rev2.0). |