Add initial documentation

This commit is contained in:
2025-07-20 21:38:21 +03:00
parent 8ba0a0ba6d
commit 42d9d2d860
24 changed files with 4447 additions and 0 deletions

View File

@ -0,0 +1,186 @@
#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). |