ha-inlite

Home Assistant integration for in-lite
git clone https://git.stephank.nl/ha-inlite
Log | Files | Refs | README | LICENSE | ZIP

cooperative-rate-limiting.md (7451B)


      1 # Cooperative Rate Limiting for Multi-Agent Deployments
      2 
      3 > Coordinate API quota across multiple Ralph instances to prevent cascading failures.
      4 
      5 ## Problem
      6 
      7 The [circuit breaker template](ralph-circuit-breaker.md) handles single-instance rate limiting well. But when multiple Ralphs run across machines (or pods on K8s), each instance independently hits API limits:
      8 
      9 - **No coordination** — 5 Ralphs each think they have full API quota
     10 - **Thundering herd** — All Ralphs retry simultaneously after rate limit resets
     11 - **Priority inversion** — Low-priority work exhausts quota before critical work runs
     12 - **Reactive only** — Circuit opens AFTER 429, wasting the failed request
     13 
     14 ## Solution: 6-Pattern Architecture
     15 
     16 These patterns layer on top of the existing circuit breaker. Each is independent — adopt one or all.
     17 
     18 ### Pattern 1: Traffic Light (RAAS — Rate-Aware Agent Scheduling)
     19 
     20 Map GitHub API `X-RateLimit-Remaining` to traffic light states:
     21 
     22 | State | Remaining % | Behavior |
     23 |-------|------------|----------|
     24 | 🟢 GREEN | >20% | Normal operation |
     25 | 🟡 AMBER | 5–20% | Only P0 agents proceed |
     26 | 🔴 RED | <5% | Block all except emergency P0 |
     27 
     28 ```typescript
     29 type TrafficLight = 'green' | 'amber' | 'red';
     30 
     31 function getTrafficLight(remaining: number, limit: number): TrafficLight {
     32   const pct = remaining / limit;
     33   if (pct > 0.20) return 'green';
     34   if (pct > 0.05) return 'amber';
     35   return 'red';
     36 }
     37 
     38 function shouldProceed(light: TrafficLight, agentPriority: number): boolean {
     39   if (light === 'green') return true;
     40   if (light === 'amber') return agentPriority === 0; // P0 only
     41   return false; // RED — block all
     42 }
     43 ```
     44 
     45 ### Pattern 2: Cooperative Token Pool (CMARP)
     46 
     47 A shared JSON file (`~/.squad/rate-pool.json`) distributes API quota:
     48 
     49 ```json
     50 {
     51   "totalLimit": 5000,
     52   "resetAt": "2026-03-22T20:00:00Z",
     53   "allocations": {
     54     "picard": { "priority": 0, "allocated": 2000, "used": 450, "leaseExpiry": "2026-03-22T19:55:00Z" },
     55     "data": { "priority": 1, "allocated": 1750, "used": 200, "leaseExpiry": "2026-03-22T19:55:00Z" },
     56     "ralph": { "priority": 2, "allocated": 1250, "used": 100, "leaseExpiry": "2026-03-22T19:55:00Z" }
     57   }
     58 }
     59 ```
     60 
     61 **Rules:**
     62 - P0 agents (Lead) get 40% of quota
     63 - P1 agents (specialists) get 35%
     64 - P2 agents (Ralph, Scribe) get 25%
     65 - Stale leases (>5 minutes without heartbeat) are auto-recovered
     66 - Each agent checks their remaining allocation before making API calls
     67 
     68 ```typescript
     69 interface RatePoolAllocation {
     70   priority: number;
     71   allocated: number;
     72   used: number;
     73   leaseExpiry: string;
     74 }
     75 
     76 interface RatePool {
     77   totalLimit: number;
     78   resetAt: string;
     79   allocations: Record<string, RatePoolAllocation>;
     80 }
     81 
     82 function canUseQuota(pool: RatePool, agentName: string): boolean {
     83   const alloc = pool.allocations[agentName];
     84   if (!alloc) return true; // Unknown agent — allow (graceful)
     85   
     86   // Reclaim stale leases from crashed agents
     87   const now = new Date();
     88   for (const [name, a] of Object.entries(pool.allocations)) {
     89     if (new Date(a.leaseExpiry) < now && name !== agentName) {
     90       a.allocated = 0; // Reclaim
     91     }
     92   }
     93   
     94   return alloc.used < alloc.allocated;
     95 }
     96 ```
     97 
     98 ### Pattern 3: Predictive Circuit Breaker (PCB)
     99 
    100 Opens the circuit BEFORE getting a 429 by predicting when quota will run out:
    101 
    102 ```typescript
    103 interface RateSample {
    104   timestamp: number;  // Date.now()
    105   remaining: number;  // from X-RateLimit-Remaining header
    106 }
    107 
    108 class PredictiveCircuitBreaker {
    109   private samples: RateSample[] = [];
    110   private readonly maxSamples = 10;
    111   private readonly warningThresholdSeconds = 120;
    112 
    113   addSample(remaining: number): void {
    114     this.samples.push({ timestamp: Date.now(), remaining });
    115     if (this.samples.length > this.maxSamples) {
    116       this.samples.shift();
    117     }
    118   }
    119 
    120   /** Predict seconds until quota exhaustion using linear regression */
    121   predictExhaustion(): number | null {
    122     if (this.samples.length < 3) return null;
    123 
    124     const n = this.samples.length;
    125     const first = this.samples[0];
    126     const last = this.samples[n - 1];
    127     
    128     const elapsedMs = last.timestamp - first.timestamp;
    129     if (elapsedMs === 0) return null;
    130     
    131     const consumedPerMs = (first.remaining - last.remaining) / elapsedMs;
    132     if (consumedPerMs <= 0) return null; // Not consuming — safe
    133     
    134     const msUntilExhausted = last.remaining / consumedPerMs;
    135     return msUntilExhausted / 1000;
    136   }
    137 
    138   shouldOpen(): boolean {
    139     const eta = this.predictExhaustion();
    140     if (eta === null) return false;
    141     return eta < this.warningThresholdSeconds;
    142   }
    143 }
    144 ```
    145 
    146 ### Pattern 4: Priority Retry Windows (PWJG)
    147 
    148 Non-overlapping jitter windows prevent thundering herd:
    149 
    150 | Priority | Retry Window | Description |
    151 |----------|-------------|-------------|
    152 | P0 (Lead) | 500ms–5s | Recovers first |
    153 | P1 (Specialists) | 2s–30s | Moderate delay |
    154 | P2 (Ralph/Scribe) | 5s–60s | Most patient |
    155 
    156 ```typescript
    157 function getRetryDelay(priority: number, attempt: number): number {
    158   const windows: Record<number, [number, number]> = {
    159     0: [500, 5000],     // P0: 500ms–5s
    160     1: [2000, 30000],   // P1: 2s–30s
    161     2: [5000, 60000],   // P2: 5s–60s
    162   };
    163   
    164   const [min, max] = windows[priority] ?? windows[2];
    165   const base = Math.min(min * Math.pow(2, attempt), max);
    166   const jitter = Math.random() * base * 0.5;
    167   return base + jitter;
    168 }
    169 ```
    170 
    171 ### Pattern 5: Resource Epoch Tracker (RET)
    172 
    173 Heartbeat-based lease system for multi-machine deployments:
    174 
    175 ```typescript
    176 interface ResourceLease {
    177   agent: string;
    178   machine: string;
    179   leaseStart: string;
    180   leaseExpiry: string;  // Typically 5 minutes from now
    181   allocated: number;
    182 }
    183 
    184 // Each agent renews its lease every 2 minutes
    185 // If lease expires (agent crashed), allocation is reclaimed
    186 ```
    187 
    188 ### Pattern 6: Cascade Dependency Detector (CDD)
    189 
    190 Track downstream failures and apply backpressure:
    191 
    192 ```
    193 Agent A (rate limited) → Agent B (waiting for A) → Agent C (waiting for B)
    194                          ↑ Backpressure signal: "don't start new work"
    195 ```
    196 
    197 When a dependency is rate-limited, upstream agents should pause new work rather than queuing requests that will fail.
    198 
    199 ## Kubernetes Integration
    200 
    201 On K8s, cooperative rate limiting can use KEDA to scale pods based on API quota:
    202 
    203 ```yaml
    204 apiVersion: keda.sh/v1alpha1
    205 kind: ScaledObject
    206 spec:
    207   scaleTargetRef:
    208     name: ralph-deployment
    209   triggers:
    210   - type: external
    211     metadata:
    212       scalerAddress: keda-copilot-scaler:6000
    213       # Scaler returns 0 when rate limited → pods scale to zero
    214 ```
    215 
    216 See [keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler) for a complete implementation.
    217 
    218 ## Quick Start
    219 
    220 1. **Minimum viable:** Adopt Pattern 1 (Traffic Light) — read `X-RateLimit-Remaining` from API responses
    221 2. **Multi-machine:** Add Pattern 2 (Cooperative Pool) — shared `rate-pool.json`
    222 3. **Production:** Add Pattern 3 (Predictive CB) — prevent 429s entirely
    223 4. **Kubernetes:** Add KEDA scaler for automatic pod scaling
    224 
    225 ## References
    226 
    227 - [Circuit Breaker Template](ralph-circuit-breaker.md) — Foundation patterns
    228 - [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Production K8s deployment
    229 - [KEDA Copilot Scaler](https://github.com/tamirdresher/keda-copilot-scaler) — Custom KEDA external scaler