test(witan): concurrency was argued from the design, never observed - #211
test(witan): concurrency was argued from the design, never observed#211blarghmatey wants to merge 2 commits into
Conversation
The multi-user deployment's write-coordination story rested on reasoning alone. That reasoning is the weak kind: `acquire_store_flock` is skipped for http(s) stores, so the advisory lock that serialises local writers does nothing on the shared server, and `task_claim` is best-effort CAS pending an upstream conditional-write. Every existing concurrency test runs in-process against a local store -- the one configuration where the lock DOES work, and not the one that ships. This adds a probe that runs N genuinely independent client processes against a deployed target, released at one wall-clock instant, and reports counts: mutual exclusion on a contended claim, lost writes under concurrent stores, and read availability during that write load. Against CI it says: mutual exclusion held at 3, 8 and 16 racers, and no write was ever lost -- but the deployment sheds ~4-5 of 16 concurrent writers at connect, and the front door answers 502 rather than 429 when it does. Both are filed separately; recording them in the module docstring so the next run has something to differ from. Two things the probe has to defend against to measure anything at all, both of them findings in their own right: a verification read that RAISES is inconclusive, not a lost write (calling it one manufactured 13 false positives on the first run), and the workers must share one pinned token, because N clients refreshing at one instant stampede the shared OIDC cache and the probe would measure that bug instead. Refs: tk-verify-the-deployed-witan-supports-concurrent-us-2da1b2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZE2vDQ1JVDxGB9oH9ZqFF
There was a problem hiding this comment.
Pull request overview
Adds a deployed concurrency probe for validating Witan’s multi-process coordination and availability behavior.
Changes:
- Adds synchronized claim, write, and read probes.
- Pins authentication tokens and reports quantitative outcomes.
- Adds cleanup support and maintainer documentation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
witan/scripts/concurrency_probe.py |
Implements the deployed concurrency harness. |
witan/scripts/__init__.py |
Documents the maintainer scripts package. |
Suppressed comments (3)
mcp/servers/witan/witan/scripts/concurrency_probe.py:421
- Probe B prints the write fire spread but does not include it in the pass condition, so staggered writes can produce PASS without exercising concurrent writers. Require all writers to be ready before the epoch and enforce an explicit spread tolerance before accepting this result.
# Only a row the server ACKED and that is then verifiably ABSENT is a
# lost write. Unverifiable reads make the probe inconclusive, not failed.
passed=not missing and not dupes and not write_errors and not unknown,
mcp/servers/witan/witan/scripts/concurrency_probe.py:365
- This second launcher also places the pinned bearer token from
plin argv, exposing it to process inspection for every writer and reader. Use the same non-argv transport (stdin or an inherited pipe) as the claim launcher.
"--payload",
json.dumps(pl),
mcp/servers/witan/witan/scripts/concurrency_probe.py:376
- The combined writer/reader collector has the same uncaught
TimeoutExpiredpath: a timed-out child is not killed or reaped, and the probe exits instead of reporting degradation. Centralize collection with_spawnand ensure timed-out processes are terminated and represented as failed rows.
write_rows, read_rows = [], []
for mode, proc in procs:
stdout, stderr = proc.communicate(timeout=300)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… concurrency Copilot's review of #211 found one class of defect ten different ways, and it is the one that matters most for an instrument whose entire job is to certify exit criterion 2: every probe could go green without the contention it claims to measure ever happening. - A passed on "exactly one winner", ignoring the fire spread its own docstring calls untrustworthy, and ignoring workers whose warmup ran past the epoch and so never raced at all. - A counted any non-claiming response as a well-behaved loser. `claimed: null`, a missing result, or a blank reason all sailed through, so one real winner could carry the probe. - B ignored spread too, and a writer that returned ok with no slug fell between `acked` and `write_errors` -- verified nothing, reported nothing. - C passed on reads alone, never checking that any read overlapped a write. A slow reader running after the storm ended counted as availability under load. - --racers 1, --writers 0 and --readers 0 all produced a confident PASS over nothing. Each probe now fails, loudly and with the reason, unless it can show the concurrency happened. That is the point of the change: a probe that cannot certify contention must never report success. Also from the same review, all verified against the real code: - The pinned bearer token was in the workers' argv, readable in `ps` and /proc/*/cmdline for the whole lead interval by design. It moves to stdin, written at spawn so warmups still overlap. - `communicate(timeout=)` raised TimeoutExpired uncaught: the probe aborted and left the worker running against the live deployment. Now killed, reaped, and recorded as a failed row. - Cleanup ran only on the happy path, so any raise after task_create left probe rows in the shared graph. Now in a finally. - memory_delete refuses by RETURNING {"deleted": false, "reason": ...} rather than raising, so the cleanup count claimed rows were gone while they were still there. It now reads the field. - The token had to cover 2*lead+180 while each phase permitted 300s, so B could fire with an expired token and measure 401s. A token is now pinned per phase, and the worker guard drops 300s -> 90s so the requirement fits inside a ~5min Keycloak token at all. The two launchers are merged into one `_launch`; the B/C copy had drifted and never handled a timeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RQxYruGCsm7s1u5enzAo8
|
Addressed all 13 findings from the Copilot review (10 threads + the 3 suppressed) in fa7e49a — 10/10 threads replied to and resolved, 0 code findings declined, 9/9 checks green. Every finding was verified against real code before acting; none turned out to be a false positive. Ten of the thirteen were one defect wearing different hats, and it is the one that matters most for this file specifically: the probe could report PASS on a run where the concurrency it claims to measure never happened. An instrument whose whole job is to certify exit criterion 2 of Most telling: So all three probes now refuse to pass unless they can show contention occurred, and print why when they cannot:
The other three were straightforward and equally real: the pinned bearer token was in worker argv (visible in Two judgment calls to flag rather than bury:
Verified behaviourally, not just by reading: the timing gate correctly rejects a late worker and a 3000ms spread; all four vacuous configs are rejected; a worker reads its payload from stdin with nothing sensitive in argv; and a stub child sleeping 60s against a 1s guard is killed and reaped in 1.0s with a |
The multi-user deployment's write-coordination story rested on reasoning alone,
and that reasoning is the weak kind:
acquire_store_flockis skipped forhttp(s) stores (
_UNLOCKABLE_SCHEMES), so the advisory lock that serialiseslocal writers does nothing on the shared server, and
task_claimis documentedbest-effort CAS pending an upstream conditional-write. Every existing
concurrency test runs in-process against a local store — the one configuration
where the lock DOES work, and not the one that ships.
This adds
witan.scripts.concurrency_probe: N genuinely independent OSprocesses (separate interpreters, MCP clients and connections) busy-wait on a
shared epoch and fire together, then report counts, not adjectives.
task_claim→ exactly oneclaimed: true, every loser a structured refusalmemory_store→ every acked row readable afterwardsIt writes real rows, tags everything
concurrency-probe, and cleans up on theway out;
--keepleaves them.What it observed against
witan.ci.ol.mit.eduRecorded in the module docstring so a later run has something to differ from:
Caveat worth keeping: every loser refused with
held, neverlost_race— thewinner's write landed before the losers' read, so the best-effort CAS retry
loop was not itself exercised even at 16-way contention.
at connect; every write the server acked was readable. It fails closed.
So: safe under concurrency, but it sheds load ungracefully above ~24 connections.
Both causes are filed separately rather than smoothed over —
tk-deployed-witan-saturates-at-24-32-concurrent-con-8e4afc(ToolHive vMCPOOMKilled at 512Mi) and
tk-concurrent-agents-stampede-the-oidc-token-refres-677984(N clients stampedethe shared OIDC token cache).
Two things the probe must defend against to measure anything at all
Both were findings in their own right:
it one manufactured 13 false positives on the first run.
LOSTandUNVERIFIABLEare now counted and reported separately, and neither passes.provider on every invoke, so N clients firing at one instant stampede the
OIDC cache — the probe would measure that bug instead of the store it is
aimed at.
Verification
just test-witan-council→ 722 passedruff check/ruff format --checkcleanmain(which carries fix(witan): two of one person's parallel sessions silently shared a task claim #208'ssession-qualified claim holders): A/B/C all PASS at 3 racers / 3 writers /
2 readers, 0ms fire spread, cleanup complete. The probe passes explicit
per-worker assignees, so fix(witan): two of one person's parallel sessions silently shared a task claim #208's holder change doesn't affect it.
Refs:
tk-verify-the-deployed-witan-supports-concurrent-us-2da1b2(phase-exit criterion 2 of
wp-witan-multi-user-service-deployment-dcf6ee,acceptance criterion 1: "a repeatable harness, committed, that any maintainer
can re-run against a deployed target").
🤖 Generated with Claude Code
https://claude.ai/code/session_015tsjX4m3AcnRNq6EBmtXV7