soma.cell

Rate limiter — admission control with a quota the memory enforces

On September 20, 2015, DynamoDB's storage servers all renewed their metadata leases at once after a network blip. The metadata service had no effective admission control; the retry storm starved it, which caused more timeouts, which caused more retries. The brownout took down large parts of us-east-1 for five hours and dragged twenty other AWS services with it. Cascading retry storms are what rate limiters exist to stop — IF the limiter itself cannot be talked out of its quota. This limiter's token bucket lives behind a memory invariant: tokens can never go below zero (no overdraft "just this once") and never above the burst ceiling (no refill bug minting infinite capacity). The breaker is a state machine with a one-way path through `shedding` and `recovering` — there is no edge from shedding straight back to closed-and-full-traffic, so the thundering herd cannot re-arrive at once.

A Soma program from the devops part of the verified corpus: 119 lines, cells RateLimiter, LimiterTests. It passes soma check with soma 3.2.4.

How it is built

The cell RateLimiter declares 6 signals in its face: admit(svc: String) -> Map, refill(svc: String, n: Int) -> Map, trip_breaker(svc: String) -> Map, start_recovery(svc: String) -> Map, breaker_close(svc: String) -> Map and tokens_left(svc: String) -> Int; keeps 1 memory slot: tokens (Map<String, Int>, persistent and consistent) under the invariant tokens >= 0 && tokens <= 10. Its state machine breaker starts in serving and allows 4 transitions: serving -> shedding, shedding -> recovering, recovering -> serving and recovering -> shedding. Handlers: admit (line 48), refill (line 60), trip_breaker (line 69), start_recovery (line 75), breaker_close (line 81) and tokens_left (line 87).

The file also holds 1 test cell (LimiterTests) with 16 assertions, run by soma test.

Generated from soma describe on this file.

The source

rate_limiter.cell

// ============================================================================
// RATE LIMITER — admission control with a quota the memory enforces
// ============================================================================
//
// On September 20, 2015, DynamoDB's storage servers all renewed their
// metadata leases at once after a network blip. The metadata service had
// no effective admission control; the retry storm starved it, which
// caused more timeouts, which caused more retries. The brownout took
// down large parts of us-east-1 for five hours and dragged twenty other
// AWS services with it. Cascading retry storms are what rate limiters
// exist to stop — IF the limiter itself cannot be talked out of its quota.
//
// This limiter's token bucket lives behind a memory invariant: tokens
// can never go below zero (no overdraft "just this once") and never
// above the burst ceiling (no refill bug minting infinite capacity).
// The breaker is a state machine with a one-way path through `shedding`
// and `recovering` — there is no edge from shedding straight back to
// closed-and-full-traffic, so the thundering herd cannot re-arrive at once.
// ============================================================================

cell RateLimiter {
    face {
        signal admit(svc: String) -> Map
        signal refill(svc: String, n: Int) -> Map
        signal trip_breaker(svc: String) -> Map
        signal start_recovery(svc: String) -> Map
        signal breaker_close(svc: String) -> Map
        signal tokens_left(svc: String) -> Int
    }

    memory {
        // burst ceiling 10, floor 0. The overdraft and the infinite-
        // refill bug are both rejected at the write.
        tokens: Map<String, Int> [persistent, consistent]
        invariant tokens >= 0 && tokens <= 10
    }

    // No shedding -> serving edge: recovery is mandatory, traffic
    // returns gradually or not at all.
    state breaker {
        initial: serving
        serving -> shedding
        shedding -> recovering
        recovering -> serving
        recovering -> shedding
    }

    on admit(svc: String) {
        if get_status(svc) == "shedding" {
            return map("ok", false, "reason", "load shedding — request dropped at the door")
        }
        let t = tokens.get(svc) ?? 0
        let w = try { tokens.set(svc, t - 1) }
        if w.error != () {
            return map("ok", false, "reason", "quota exhausted ({t} tokens)")
        }
        return map("ok", true, "tokens_left", t - 1)
    }

    on refill(svc: String, n: Int) {
        let t = tokens.get(svc) ?? 0
        let w = try { tokens.set(svc, t + n) }
        if w.error != () {
            return map("ok", false, "reason", "refill past burst ceiling refused")
        }
        return map("ok", true, "tokens_left", t + n)
    }

    on trip_breaker(svc: String) {
        let r = try { transition(svc, "shedding") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", "shedding")
    }

    on start_recovery(svc: String) {
        let r = try { transition(svc, "recovering") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", "recovering")
    }

    on breaker_close(svc: String) {
        let r = try { transition(svc, "serving") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", "serving")
    }

    on tokens_left(svc: String) {
        return tokens.get(svc) ?? 0
    }
}

cell test LimiterTests {
    rules {
        // a full bucket admits, then runs dry — and the dry bucket
        // cannot be overdrawn: the -1 write is refused by memory
        assert refill("meta", 3).ok == true
        assert admit("meta").ok == true
        assert admit("meta").ok == true
        assert admit("meta").ok == true
        assert admit("meta").ok == false
        assert tokens_left("meta") == 0
        assert_fails tokens.set("meta", -1)

        // the infinite-refill bug: minting past the burst ceiling
        assert refill("meta", 10).ok == true
        assert refill("meta", 5).ok == false
        assert_fails tokens.set("meta", 999)

        // THE DYNAMODB MOVE: storm hits, breaker trips, and there is
        // no edge that snaps full traffic back on in one step
        assert trip_breaker("meta").ok == true
        assert admit("meta").ok == false
        assert_fails transition("meta", "serving")
        assert start_recovery("meta").ok == true
        assert breaker_close("meta").ok == true
        assert admit("meta").ok == true
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check rate_limiter.cell
✓ All checks passed.
$ soma test rate_limiter.cell
test LimiterTests ...
  ✓ assert refill("meta", 3).ok == true
  ✓ assert admit("meta").ok == true
  ✓ assert admit("meta").ok == true
  ✓ assert admit("meta").ok == true
  ✓ assert admit("meta").ok == false
  ✓ assert tokens_left("meta") == 0
  ✓ assert_fails tokens.set("meta", -1) — raised memory invariant violated on 'tokens': tokens >= 0 && tokens <= 10 — rejected write of -1 (key "meta"); the slot is unchanged
  ✓ assert refill("meta", 10).ok == true
  ✓ assert refill("meta", 5).ok == false
  ✓ assert_fails tokens.set("meta", 999) — raised memory invariant violated on 'tokens': tokens >= 0 && tokens <= 10 — rejected write of 999 (key "meta"); the slot is unchanged
  ✓ assert trip_breaker("meta").ok == true
  ✓ assert admit("meta").ok == false
  ✓ assert_fails transition("meta", "serving") — raised invalid transition: shedding → serving. Current state: 'shedding'. Valid targets: [recovering]
  ✓ assert start_recovery("meta").ok == true
  ✓ assert breaker_close("meta").ok == true
  ✓ assert admit("meta").ok == true

16 tests: 16 passed, 0 failed
$ soma verify rate_limiter.cell
Verifying rate_limiter.cell...

State machine 'breaker': 3 states, initial 'serving'
  States: [recovering, serving, shedding]

  ✓ 3 states, 4 transitions
  ✓ all states reachable from 'serving'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'serving'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'serving' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (3 handlers, 3 literal transitions, 0 dynamic)
  ✓ termination: all 6 handlers structurally terminate
  ✓ refinement: handler `trip_breaker` ⟶ {shedding}
  ✓ refinement: handler `start_recovery` ⟶ {recovering}
  ✓ refinement: handler `breaker_close` ⟶ {serving}
  · transition sources: `trip_breaker`, `start_recovery`, `breaker_close` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)

RateLimiter/invariants:

  ✓ invariant tokens <= 10 — writer 'admit' proven by induction (writes t - 1)
  ⚠ invariant tokens >= 0 && tokens <= 10 — runtime-checked (computed values): admit → tokens [tokens >= 0] because `t - 1` is only known to lie in [-1, 9] — narrow it: `require (t - 1) >= 0 else …`, refill → tokens because `n` is a parameter (narrow it: `require (t + n) >= 0 && (t + n) <= 10 else …`)

11 passed, 1 warnings, 0 failures

  Temporal properties for 'breaker':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/devops/rate_limiter.cell · examples/corpus/devops/rate_limiter.cell