fix(benchmarks): auto-drain and dispatch pending runs on agent ws connect - #209
fix(benchmarks): auto-drain and dispatch pending runs on agent ws connect#209JLCode-tech wants to merge 5 commits into
Conversation
…nect drain
MAJOR-1: the initial POST dispatch marked the first child RUNNING with a plain
ORM write committed only AFTER the blocking dispatch_to_agent round-trip, while
the group+children were already committed PENDING. A WS (re)connect firing in
that window found the row PENDING, won claim_pending_run, and sent a SECOND
{"type":"run"} for the same run. Now the initial dispatch claims the child
ATOMICALLY (claim_pending_run, group-guarded) and PERSISTS the claim BEFORE the
send round-trip, and reverts on send failure -- so initial-dispatch and
connect-drain are mutually exclusive on the row; the loser skips.
MAJOR-2: the connect-drain guarded agent-wide while _dispatch_next_group_child
claimed next-in-group, so on a run_completed+reconnect interleave the two paths
could claim different sibling rows and put two children of one group RUNNING.
claim_pending_run now takes group_id and adds a NOT-EXISTS group-sequential
guard (refuse if any sibling is RUNNING); _dispatch_next_group_child uses it, and
the connect-drain routes grouped runs through _dispatch_next_group_child -- one
serialization point, so two siblings can never both be RUNNING.
MINOR-3: add deterministic state-level tests for the group guard (two siblings
can't both be RUNNING), the MAJOR-1 initial-vs-drain claim race, and async tests
for the drain's claim -> send_command_to_agent -> group PENDING->RUNNING path and
the release_claimed_run rollback on send failure.
MINOR-4: pre-existing WS-identity weakness (drain auto-sends a run config to any
JWT socket when BENCHMARK_AGENT_AUTH_REQUIRED is off) left for a separate issue --
a matching-agent_id guard would reject the flag-off built-in agent (whose token
legitimately carries no agent_id claim), so it is not a safe one-liner here.
Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
Self-review (cold, adversarial) + fixes appliedIndependent cold audit, executed at the state level. It found a MAJOR double-dispatch race (no blocker), now fixed @ MAJOR 1 — initial POST dispatch vs connect-drain double-dispatch — FIXED. The initial dispatch marked the first child RUNNING with a plain ORM write committed only AFTER the blocking MAJOR 2 — two children of one group RUNNING at once — FIXED. MINOR 3 — coverage — FIXED: 11 deterministic tests — the group guard, the initial-vs-drain same-row race, and the drain path (claim→send→group-flip + release-on-failure + skip-when-sibling-running). Mutation-tested. MINOR 4 — config-gated impersonation — deferred with reasoning: the drain auto-dispatching a run's config to any valid-JWT socket when Verified: 120 passed, ruff clean, no contract change (WS routes aren't in the spec). Ready for review. |
Review —
|
…n_for_agent tests
…nect drain
MAJOR-1: the initial POST dispatch marked the first child RUNNING with a plain
ORM write committed only AFTER the blocking dispatch_to_agent round-trip, while
the group+children were already committed PENDING. A WS (re)connect firing in
that window found the row PENDING, won claim_pending_run, and sent a SECOND
{"type":"run"} for the same run. Now the initial dispatch claims the child
ATOMICALLY (claim_pending_run, group-guarded) and PERSISTS the claim BEFORE the
send round-trip, and reverts on send failure -- so initial-dispatch and
connect-drain are mutually exclusive on the row; the loser skips.
MAJOR-2: the connect-drain guarded agent-wide while _dispatch_next_group_child
claimed next-in-group, so on a run_completed+reconnect interleave the two paths
could claim different sibling rows and put two children of one group RUNNING.
claim_pending_run now takes group_id and adds a NOT-EXISTS group-sequential
guard (refuse if any sibling is RUNNING); _dispatch_next_group_child uses it, and
the connect-drain routes grouped runs through _dispatch_next_group_child -- one
serialization point, so two siblings can never both be RUNNING.
MINOR-3: add deterministic state-level tests for the group guard (two siblings
can't both be RUNNING), the MAJOR-1 initial-vs-drain claim race, and async tests
for the drain's claim -> send_command_to_agent -> group PENDING->RUNNING path and
the release_claimed_run rollback on send failure.
MINOR-4: pre-existing WS-identity weakness (drain auto-sends a run config to any
JWT socket when BENCHMARK_AGENT_AUTH_REQUIRED is off) left for a separate issue --
a matching-agent_id guard would reject the flag-off built-in agent (whose token
legitimately carries no agent_id claim), so it is not a safe one-liner here.
Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
… send - MAJOR-A (INV-8): Lock BenchmarkRunGroup row with with_for_update() in claim_pending_run to serialize sibling claims across concurrent transactions under PostgreSQL READ COMMITTED - MINOR-B: Commit claimed run state before awaiting WebSocket send in _dispatch_next_group_child and release+commit on failure - MINOR-C: Remove duplicate .trivyignore entry now included in staging - NIT-D: Atomically transition run-group PENDING to RUNNING in connect-drain via mark_run_group_running_if_pending
a534e20 to
b095349
Compare
Resolution of
|
…nect drain
MAJOR-1: the initial POST dispatch marked the first child RUNNING with a plain
ORM write committed only AFTER the blocking dispatch_to_agent round-trip, while
the group+children were already committed PENDING. A WS (re)connect firing in
that window found the row PENDING, won claim_pending_run, and sent a SECOND
{"type":"run"} for the same run. Now the initial dispatch claims the child
ATOMICALLY (claim_pending_run, group-guarded) and PERSISTS the claim BEFORE the
send round-trip, and reverts on send failure -- so initial-dispatch and
connect-drain are mutually exclusive on the row; the loser skips.
MAJOR-2: the connect-drain guarded agent-wide while _dispatch_next_group_child
claimed next-in-group, so on a run_completed+reconnect interleave the two paths
could claim different sibling rows and put two children of one group RUNNING.
claim_pending_run now takes group_id and adds a NOT-EXISTS group-sequential
guard (refuse if any sibling is RUNNING); _dispatch_next_group_child uses it, and
the connect-drain routes grouped runs through _dispatch_next_group_child -- one
serialization point, so two siblings can never both be RUNNING.
MINOR-3: add deterministic state-level tests for the group guard (two siblings
can't both be RUNNING), the MAJOR-1 initial-vs-drain claim race, and async tests
for the drain's claim -> send_command_to_agent -> group PENDING->RUNNING path and
the release_claimed_run rollback on send failure.
MINOR-4: pre-existing WS-identity weakness (drain auto-sends a run config to any
JWT socket when BENCHMARK_AGENT_AUTH_REQUIRED is off) left for a separate issue --
a matching-agent_id guard would reject the flag-off built-in agent (whose token
legitimately carries no agent_id claim), so it is not a safe one-liner here.
Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
Summary
/ws/benchmarks/agents/{id}), the server immediately queries for anypendingruns assigned to that agent and dispatches the earliest pending run.PENDING->RUNNINGatomically with rollback safety if dispatch fails.backend/tests/unit/test_benchmark_service_run_groups.pyand documented WebSocket endpoint indocs/API_REFERENCE.md.