Live server: stop ends the process, SSE skips the mutation lane

Two Rust-only regressions found while investigating #719, both of which
can leave a tab waiting on a broadcast that never comes.

/stop ran shutdown() but never set shutting_down, and the accept loop
only breaks on that flag or a signal, so a stopped server kept its port
and kept answering while its server.json was already deleted. The next
`impeccable live` then booted a second server on another port and a tab
could reattach to the zombie. Node's shutdown() ended in process.exit(0).
The flag is now set after the response is written, so `stop` still reads
"stopping" instead of a reset connection, and the accept loop (already
non-blocking) exits on its next pass.

GET /events took a turnstile ticket and waited its turn before
registering, even though handle_sse releases that ticket two statements
later and needs no arrival ordering. A peer that stalls mid-request holds
the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream
could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s
on Node); broadcast is fire-and-forget, so a `done` landing in that
window reaches an empty client set and is gone. Registering early can
only make a stream see more broadcasts. The one cost is that the
connected frame's activeSessions snapshot may miss a mutation still in
flight, and the browser treats that snapshot as a hint. Preflights still
take a turn: answering those out of order reorders the POSTs the browser
issues behind them.

The route classification moved into releases_ticket_up_front so it can be
unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped
server's pid is gone and its port is free.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
Paul Bakaus
2026-09-04 00:10:03 -07:00
co-authored by Claude Code
parent 5e626e2d9f
commit 26cb1f0193
2 changed files with 132 additions and 11 deletions
+55
View File
@@ -37,6 +37,8 @@ const WINDOWS = process.platform === 'win32';
const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url));
const PORT = 8591;
// A second port so the stop guard never collides with the reaper guard.
const STOP_PORT = 8592;
// The server under test is the engine's `live-server` verb, not a script: this
// guarantee has to hold for whatever binary the harness is pointed at.
const ENGINE_BIN = findEngineBinary();
@@ -150,6 +152,59 @@ describe('live server leak guard', () => {
}
});
it('ends the server process on stop, so the port is free again', {
skip: NO_ENGINE,
}, async () => {
// Node's shutdown() finished with process.exit(0). The engine's did not,
// and nothing set `shutting_down`, so a stopped server kept the port and
// kept answering while its server.json was already gone: the next
// `impeccable live` booted a second server elsewhere and a tab could
// reattach to the zombie and never hear another broadcast (issue #719).
const cwd = mkdtempSync(join(tmpdir(), 'impeccable-stop-'));
let server;
try {
writeFileSync(join(cwd, 'package.json'), '{"name":"stop-fixture"}\n');
server = spawn(ENGINE_BIN, ['live-server', `--port=${STOP_PORT}`], {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
IMPECCABLE_SKILL_DIR: join(REPO_ROOT, 'skill'),
IMPECCABLE_SELF: ENGINE_BIN,
},
});
const up = await waitUntil(async () => {
try {
const res = await fetch(`http://127.0.0.1:${STOP_PORT}/health`);
return res.ok || res.status === 401;
} catch { return false; }
}, { timeoutMs: 20_000 });
assert.equal(up, true, 'live server never came up');
const stop = spawn(ENGINE_BIN, ['live-server', 'stop'], {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
IMPECCABLE_SKILL_DIR: join(REPO_ROOT, 'skill'),
IMPECCABLE_SELF: ENGINE_BIN,
},
});
await new Promise((resolve) => stop.on('exit', resolve));
const gone = await waitUntil(() => !alive(server.pid), { timeoutMs: 15_000 });
assert.equal(gone, true, `live server pid ${server.pid} survived its own stop`);
const stillServing = await fetch(`http://127.0.0.1:${STOP_PORT}/health`)
.then(() => true)
.catch(() => false);
assert.equal(stillServing, false, 'stopped server is still answering on its port');
} finally {
if (server && server.exitCode == null) server.kill('SIGKILL');
rmSync(cwd, { recursive: true, force: true });
}
});
it('scopes a sweep to the marker, never to the name alone', () => {
// No marker, no match: a sweep can never take out a live server that some
// other checkout, or the user's own session, is running.