feat(mcp): make the initialize limiter observable, and refill in minutes #23
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat-limiter-observable"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #20.
What is true, and what is not
Not claimed: that we observed this limiter refusing Julian. We did not. The 429 seen from the client reads
{"message":"Anthropic Proxy: Invalid content from server"}, which names the connector's proxy as the speaker and is equally consistent with its own rate limiting. Lucy was not a control either: hercarddavmount answerswhoamiasjulian@kampong.social, the same identity, and this limiter keys on the Logtosub.What is claimed, all of it a fact about this code:
Retry-After, no error class.governorand discarded at the boundary —NotUntil::wait_time_fromexisted all along andcheckmapped it to a unit struct.tool_calls_totalreads zero during exactly this failure, because the limiter is Axum middleware outside the MCP router and a refusal never reaches the code that increments it. In the Infrastructure lead's words, from the investigation: "it measured a limiter that was not firing while the one that was firing recorded nothing."None of that needs a 429 sighting.
The refill is the change that matters
At one slot per thirty minutes, someone who waits out what they believe is the window gets exactly one session, reconnects twice and is refused again. A wrong model of a limiter does not merely delay a user, it guarantees a second failure after the wait. That is why five attempts produced five identical failures.
Why 24 and not 32, and why the pool did not move
This is the fallback, taken deliberately. Burst 32 with
MAX_SESSIONSat 256 takes the identities needed to exhaust the pool from 32 down to 8 — a per-user refusal converted into a global exhaustion, invisible in the diff, breaking somebody other than the person who triggered it.Moving the pool with it requires the per-session memory measurement, and measuring means opening 32 authenticated sessions against a running pod, which needs a bearer this repo cannot currently obtain: Stalwart's directory is OIDC, so no password authenticates for anybody, which is the same blocker as #19.
So: 24 covers the observed workload exactly and moves the ratio to 10 identities knowingly. 1024 with burst 32 is the measured version, once a bearer exists.
Observability
tracing::warn!on the refusal branch withscope,user_hashandretry_after_seconds.user_hashis the domain-separated identity hash, which reverses against a candidate list and would have answered "whose refusal is this" in one grep. Verified by reproducing it:identity:julian@kampong.socialgivesf5a076c6a6c82848.caldav_mcp_initialize_refusals_total{scope}.Retry-Afterfrom the bucket, rounded up.scopeseparates three diagnoses that were one value:bearer(one token reconnecting),subject(one identity across rotated tokens — the shape #21 describes), andbucket_capacity, which is not a quota refusal at all.Verified by mutation, including one that found a useless test
a_refusal_names_the_subject_bucketa_refusal_names_the_bearer_bucketrefusal_scopes_have_distinct_log_valuesretry_after_secsrounds downretry_after_seconds_rounds_upquota.replenish_interval()a_quota_refusal_carries_a_wait_within_the_refill_periodThe last row survived on the first attempt. The test asserted
wait <= 60s, and the quota's replenish interval is exactly 60s, so it accepted the substitution it existed to forbid. Tightened towait < 60s: time passes between spending the cell and asking, and only the bucket knows it. A test that permits the mutation it was written against is the same object as a green that carries no information, one level down.Gates
cargo fmt --check,cargo clippy -D warnings,cargo test --all-features(116 + 9),cargo audit,cargo deny check bans licenses sources— green onrustc 1.98.0, through the shared build slot.After merge
chore(release): prepare v0.2.0, tag, image, Renovate, pod digest. Production has not moved in forty hours and runssha256:3d37a2eb; the digest goes to Clark and Alan when it lands.