You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make PaidUp resilient to a slow or failing external dependency (primarily the Anthropic API, secondarily the Parliament APIs), and build a cost-free, no-real-traffic way to load-test it.
Two outcomes:
A circuit breaker around the Claude calls so a slow/dead dependency fails fast instead of exhausting worker threads.
A fake-upstream + Locust harness so we can generate traffic and inject faults without ever calling real Claude (no cost) or hammering the public Parliament API.
Why
A single /lookup fans out to 3 external APIs in parallel; /analyze calls Anthropic. If any one goes slow (not down — slow), worker threads block and the whole app degrades, even for cached requests.
The Anthropic client currently has no explicit timeout (SDK default ~10 min) — a hang can pin a thread for minutes. This is the prerequisite to fix: a breaker is useless without timeouts.
Design
Stability patterns compose, innermost → outermost: timeout → (retry) → circuit breaker → fallback. We implement timeout + breaker + fallback now; retry/bulkhead and the chatbot fallback-chain are future work.
The breaker is a small, dependency-free, thread-safe module (State machine inside, Proxy/Decorator outside). Faking is done at the network boundary (a local fake server the app talks to), not in-process — so the real timeout/exception code path is exercised and the breaker is genuinely validated.
2. Timeout + wire into ai.py — add explicit timeout to the Anthropic client; route the analyze() Claude call through a shared _anthropic_breaker
3. Graceful fallback in main.py — catch CircuitOpenError in /analyze, return a clean 503 ("high demand, try again shortly")
4. Fault-injection unit tests — tests/test_circuit_breaker.py: closed→open after N failures, fail-fast while open, half-open trial, success resets. (This is the cheap, deterministic "crash test".)
5. Env-configurable base URLs — parliament.py, theyworkforyou.py, ai.py read base URLs from env with current values as defaults (production unchanged)
6. Fake upstream server — loadtest/fake_upstream.py: mimics Members/Interests/TWFY/Anthropic endpoints with canned responses + latency_ms and fail_rate knobs
7. Locust file — loadtest/locustfile.py: drives /lookup (heavy) and /analyze (light), all against the fake
8. Run + tune — load test against fakes, watch latency/error rate, tune breaker thresholds; dial up fail_rate to confirm the breaker trips and recovers
Out of scope (future)
Retry with exponential backoff + jitter
Bulkhead (separate thread pools for /analyze vs /lookup)
Chatbot fallback chain for the future /ask section: Sonnet → Haiku → cached → message, with error-type-aware handling (429 backoff vs 500/timeout trip) and streaming-aware failure detection
Branch
feat/circuit-breaker
Testing
Correctness: fault-injection unit tests (step 4) — no network, deterministic
Effectiveness: Locust against fake_upstream (step 8) — no cost, no real API load
Goal
Make PaidUp resilient to a slow or failing external dependency (primarily the Anthropic API, secondarily the Parliament APIs), and build a cost-free, no-real-traffic way to load-test it.
Two outcomes:
Why
/lookupfans out to 3 external APIs in parallel;/analyzecalls Anthropic. If any one goes slow (not down — slow), worker threads block and the whole app degrades, even for cached requests.Design
Stability patterns compose, innermost → outermost: timeout → (retry) → circuit breaker → fallback. We implement timeout + breaker + fallback now; retry/bulkhead and the chatbot fallback-chain are future work.
The breaker is a small, dependency-free, thread-safe module (State machine inside, Proxy/Decorator outside). Faking is done at the network boundary (a local fake server the app talks to), not in-process — so the real timeout/exception code path is exercised and the breaker is genuinely validated.
Steps
src/app/circuit_breaker.py(CLOSED/OPEN/HALF_OPEN, thread-safe, configurablefailure_threshold/recovery_timeout/expected_exception)ai.py— add explicittimeoutto the Anthropic client; route theanalyze()Claude call through a shared_anthropic_breakermain.py— catchCircuitOpenErrorin/analyze, return a clean 503 ("high demand, try again shortly")tests/test_circuit_breaker.py: closed→open after N failures, fail-fast while open, half-open trial, success resets. (This is the cheap, deterministic "crash test".)parliament.py,theyworkforyou.py,ai.pyread base URLs from env with current values as defaults (production unchanged)loadtest/fake_upstream.py: mimics Members/Interests/TWFY/Anthropic endpoints with canned responses +latency_msandfail_rateknobsloadtest/locustfile.py: drives/lookup(heavy) and/analyze(light), all against the fakefail_rateto confirm the breaker trips and recoversOut of scope (future)
/analyzevs/lookup)/asksection: Sonnet → Haiku → cached → message, with error-type-aware handling (429 backoff vs 500/timeout trip) and streaming-aware failure detectionBranch
feat/circuit-breakerTesting