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

5.4 KiB
Raw Blame History

#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

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

{
  "quota:abc123:2025-07-18": "198",
  "quota:abc123:2025-07-18-RESET": "2025-07-19T00:00:00Z"
}

## 3 QuotaVerdict DTO

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):

{
  "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+)

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).