Make debug command enqueue atomic with the engine's resume#268
Conversation
The debug verbs did an unsynchronized paused check-then-enqueue while the command loop exits on the first Continue/Step without draining the queue. Two failure modes for a verb that lost the race with a resume (UI-1822): its command stranded - Done never signalled, the caller blocked forever (which since the Runner teardown gate hangs Destroy and wedges the MCP server's closeSession under s.mu) - and the stale command lingered in the queue and replayed at the NEXT pause, silently resuming a breakpoint the user never continued. Drain-at-exit alone can't fix it: a verb that passed the check can enqueue after any drain. - _debugSync makes each verb's check+enqueue atomic with the loop's resume+drain. Every loop exit (resume, exception, disposed-while- paused) goes through ResumeAndDrainDebugCommands: clear state, then fail anything still queued with an error, under the same lock. A command observed-paused-and-enqueued is therefore always either consumed or failed - never stranded, never carried across a pause. - All eight verbs check cmd.Error after their wait, so a drained resume surfaces as an error to the caller instead of a silent no-op. - Dispose drains as a belt-and-braces, and an enqueue that races CompleteAdding gets the verb's own refusal message. - Tests: 25x3 racing Continues with Join timeouts as strand detectors, and a stale-replay guard asserting the next pause waits for its own resume. Validated end-to-end: published runtime + cli dap/mcpserver suites and bindings under -race. - Go-side comments that documented the hazard now state the guarantee.
PR Summary by QodoMake debug verb enqueue atomic with resume to prevent stranded/stale commands
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
Qodo: on a regression, the strand leaves the racer/feed threads blocked past the failed Join assert, and foreground threads then hang the test host at exit - a CI job timeout instead of a reported test failure. On the fixed code Dispose's drain releases them either way; IsBackground only changes how a future regression fails.
a debug verb racing a resume could strand its caller forever and then replay as a stale command at the next pause. The verbs did an unsynchronized paused check-then-enqueue while the command loop exits on the first Continue/Step without draining the queue; since #267's teardown gate, the stranded caller holds a session op, so the hang propagated to
Runner.Destroyand wedged the MCP server'scloseSessionunder its mutex. Drain-at-exit alone can't fix it - a verb that passed the check can enqueue after any drain - so the enqueue and the resume are made mutually atomic.JintProjectionHandler.cs-_debugSyncmakes each verb's check+enqueue atomic with the command loop's resume+drain. Every loop exit (resume, exception, disposed-while-paused) goes throughResumeAndDrainDebugCommands: clear the pause state, then fail anything still queued, under the same lock - so an enqueued command is always either consumed or failed, never stranded and never carried across a pause (a staleContinuewould have silently resumed a breakpoint the user never continued). All eight verbs check the command's error after their wait;Disposedrains as a backstop; an enqueue racingCompleteAddinggets the verb's own refusal message rather than the framework's.DebugTests.cs- two regression tests: 25 iterations of 3 racing continues with join timeouts as strand detectors (fails pre-fix on the first iteration), and a stale-replay guard asserting the next pause waits for its own resume.engine/runner_debug.go- the doc comments that recorded the hazard now state the guarantee (a verb always returns, soDestroy's wait can't wedge on one)..changeset/debug-command-strand.md- patch; the fix ships in the CLI's bundled runtime.