0. Alert first, architecture second
At 09:14 the dashboard looks healthy: CPU 38%, Redis memory steady, workers polling. The contradictory signal is smaller and worse: queue_age_p95_seconds crosses 25s while provider_5xx_rate is still only 2.1%. No page fires yet because the alert is wired to utilization, not deadline risk.
Which action follows from that evidence: add workers, restart workers, or stop admitting new agent tasks and drain the in-flight ones?
My rule: when queue age rises while provider errors are still low, treat it as an early degradation drill. Freeze admission, snapshot the queue, drain by deadline class, and roll back only after a canary proves the path is clean.
I used MonkeyCode's free model access to review an early version of the failure matrix and its free server option as a throwaway place to think through the control-plane flow; neither is required for the harness below. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Do not run the load test against any free tier; keep it local or on your own disposable infra.
1. Topology for a local drill
Run this on one Linux host with Docker. Declared lab workload: 1 Redis queue, 3 workers, 1 admission controller, 1 fake provider endpoint behind Toxiproxy. Job mix: 70% lint-fix with 60s deadline, 25% test-patch with 180s deadline, 5% refactor-plan with 900s deadline.
# docker-compose.yaml
services:
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "no"]
ports: ["6379:6379"]
toxiproxy:
image: ghcr.io/shopify/toxiproxy:latest
ports: ["8474:8474", "18080:18080"]
fake-provider:
image: python:3.12-alpine
working_dir: /app
volumes: ["./provider.py:/app/provider.py"]
command: ["python", "provider.py"]
environment:
PORT: "8080"
worker:
image: python:3.12-alpine
working_dir: /app
volumes: ["./worker.py:/app/worker.py"]
command: ["python", "worker.py"]
deploy:
replicas: 3
environment:
REDIS_URL: redis://redis:6379/0
PROVIDER_URL: http://toxiproxy:18080/v1/complete
WORKER_MAX_INFLIGHT: "2"
DEADLINE_SWEEP_MS: "250"
controller:
image: python:3.12-alpine
working_dir: /app
volumes: ["./controller.py:/app/controller.py"]
command: ["python", "controller.py"]
environment:
REDIS_URL: redis://redis:6379/0
ADMIT_QUEUE_AGE_P95_MAX_S: "20"
DRAIN_PROVIDER_5XX_RATE: "0.05"
DRAIN_QUEUE_AGE_P95_S: "25"
CANARY_REQUIRED_OK: "3"
Create a Toxiproxy route from toxiproxy:18080 to fake-provider:8080, then add latency and a 5xx fault during the drill. The provider is intentionally dumb: it returns {"ok":true} after a sleep that can be forced high.
2. Worker contract: deadline, lease, and exit reason
Every job carries job_id, class, deadline_ts, attempt, and idempotency_key. Workers never delete a job after provider timeout; they requeue with attempt+1 unless now > deadline_ts - 2s, in which case they emit job_expired and move it to failed:{date}.
# worker.py excerpt
def run_job(job):
started = time.time()
remaining = job["deadline_ts"] - started
if remaining < 2:
emit("job_expired", job_id=job["job_id"], queue_class=job["class"],
deadline_slack_s=round(remaining, 3), attempt=job["attempt"])
return "expired"
try:
resp = post_provider(job, timeout=min(10, max(1, remaining - 1)))
emit("provider_call", job_id=job["job_id"], status=resp.status_code,
latency_s=round(time.time() - started, 3), attempt=job["attempt"])
return "done" if resp.status_code == 200 else "retry"
except Timeout:
emit("provider_timeout", job_id=job["job_id"], attempt=job["attempt"],
timeout_s=round(min(10, max(1, remaining - 1)), 3))
return "retry"
Telemetry fields to keep stable across tools: queue_age_p95_seconds, oldest_job_age_seconds, deadline_slack_p10_seconds, provider_5xx_rate, provider_timeout_rate, worker_inflight, drain_state, canary_ok_count, rollback_reason. If a metric cannot answer "admit, drain, or rollback?", it is decoration.
3. Admission controller: reject before the SLO is the incident
The controller is the only writer allowed to set drain_state. Workers read it but do not set it. That keeps recovery auditable.
# controller.py excerpt
def decide(m):
if m["rollback_active"]:
return "hold", "rollback_active"
if m["canary_ok_count"] < CANARY_REQUIRED_OK and m["drain_state"] == "recovering":
return "hold", "canary_not_green"
if m["provider_5xx_rate"] >= DRAIN_PROVIDER_5XX_RATE or m["queue_age_p95_seconds"] >= DRAIN_QUEUE_AGE_P95_S:
return "drain", "provider_or_queue_age"
if m["queue_age_p95_seconds"] >= ADMIT_QUEUE_AGE_P95_MAX_S or m["deadline_slack_p10_seconds"] < 5:
return "reject_new", "admission_threshold"
return "admit", "normal"
Threshold rationale: queue age is the leading indicator, utilization is the lagging indicator, and deadline slack is the customer-facing risk. I would rather reject new work at queue_age_p95_seconds >= 20 than discover at 45s that every worker is busy on already-doomed jobs.
4. Fault injection and expected evidence
Representative output only; your numbers will differ. This is a drill harness, not a benchmark.
docker compose up -d --build
curl -X POST localhost:8474/proxies \
-d '{"name":"provider","listen":"0.0.0.0:18080","upstream":"fake-provider:8080"}'
./load.py --rate 12/s --duration 180s --mix lint-fix:70,test-patch:25,refactor-plan:5
Healthy gate, expected shape:
state=admit queue_age_p95_seconds=6.8 deadline_slack_p10_seconds=41.2 provider_5xx_rate=0.000 worker_inflight=6
Inject provider trouble:
curl -X POST localhost:8474/proxies/provider/toxics \
-d '{"name":"latency","type":"latency","attributes":{"latency":3500,"jitter":700}}'
curl -X POST localhost:8474/proxies/provider/toxics \
-d '{"name":"errors","type":"http","attributes":{"status":503,"probability":0.08}}'
Expected transition:
state=reject_new reason=admission_threshold queue_age_p95_seconds=21.7 deadline_slack_p10_seconds=9.4
state=drain reason=provider_or_queue_age provider_5xx_rate=0.081 queue_age_p95_seconds=27.9 oldest_job_age_seconds=63.1
worker action=stop_pull lease_released=14 expired=3 requeued=11
Observed behavior means the counters above came from Redis and logs in the drill. Architectural inference: the provider is not yet hard-down, but the deadline margin is already gone, so more replicas would mostly increase doomed in-flight work.
5. Drain and recovery drill
Drain order matters. Do not kill workers globally; stop pulls first, let short-deadline jobs expire explicitly, and preserve idempotency keys so clients can safely retry later.
- Set
drain_state=drainandadmission=closed. - Snapshot:
LRANGE queue:pending 0 -1,ZRANGE queue:delayed 0 -1 WITHSCORES,HGETALL job:{id}for in-flight leases. - Workers finish current call only if
deadline_slack_s > provider_timeout_s + 1; otherwise requeue or expire. - Remove Toxiproxy faults or point
PROVIDER_URLat the known-good endpoint. - Set
drain_state=recovering; run canary jobs one per class. - Require
CANARY_REQUIRED_OK=3consecutive successes anddeadline_slack_p10_seconds > 15beforeadmission=open.
Rollback path if recovery goes bad:
docker compose exec redis redis-cli SET drain_state drain
docker compose exec redis redis-cli SET admission closed
docker compose up -d --force-recreate worker
# restore last good env
sed -i 's/PROVIDER_URL=.*/PROVIDER_URL=http:\/\/toxiproxy:18080\/v1\/complete/' .env
Rollback reason must be written to rollback_reason: canary_failed, queue_age_not_recovering, duplicate_side_effect, or manual_operator_hold. If you cannot name the reason, do not reopen admission.
6. Decision table
| Signal combination | Action | Why |
|---|---|---|
| CPU high, queue age flat, slack high | Observe only | Utilization without deadline risk is not an incident |
| Queue age rising, 5xx low | Reject new, prepare drain | Backlog is forming before hard failure |
| 5xx high or timeouts high | Drain now | Retries amplify provider pain |
| Recovery canary passes, slack still low | Hold admission | Provider works but deadlines are still unsafe |
| Duplicate side effects found after recovery | Roll back to closed and audit idempotency keys | Correctness beats availability |
7. Limitations and who should not use this
This harness proves control-plane behavior, not model quality, provider capacity, or customer latency. It will not catch prompt regressions, token-cost spikes, region-specific provider failures, or database contention outside the queue path. Do not use free or shared hosted capacity for soak tests, secret-bearing jobs, regulated data, or as evidence that production can absorb the same rate. If your jobs have irreversible side effects without idempotency keys, fix that before practicing drain.
Cleanup after the drill:
docker compose down -v
docker volume prune -f
rm -rf ./snapshots ./failed-jobs.jsonl
If you want a concrete next step, run the same drill against one non-critical queue this week and tune exactly one threshold: ADMIT_QUEUE_AGE_P95_MAX_S. Keep the change small enough that the next alert tells you whether it helped.
Top comments (0)