fix(worker): make user-box lifecycle concurrency-safe

This commit is contained in:
Gabriel Brown
2026-07-10 16:54:55 -04:00
parent daec4d6dfa
commit 9e7a8722f3
6 changed files with 203 additions and 41 deletions
+12 -6
View File
@@ -4,9 +4,10 @@ import type { Server } from 'node:http';
import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws';
import type { BoxHandle } from './user-container';
import { env } from './env';
import { verifyTerminalToken } from './terminal-token';
import { acquireUserBox, releaseUserBox } from './user-container';
import { acquireUserBox } from './user-container';
import { getTerminalWorkspace } from './worker';
const clampDimension = (value: unknown) => {
@@ -63,7 +64,7 @@ const bridge = async (ws: WebSocket, jobId: string) => {
else pendingInput.push(data);
});
let acquired = false;
const handleHolder: { current?: BoxHandle } = {};
let released = false;
// Read through a function so TS doesn't narrow `released` to a constant — the
// cleanup handler flips it asynchronously when the socket closes.
@@ -72,7 +73,7 @@ const bridge = async (ws: WebSocket, jobId: string) => {
if (released) return;
released = true;
procHolder.current?.kill();
if (acquired) releaseUserBox(workspace.username);
handleHolder.current?.release();
};
ws.on('close', cleanup);
ws.on('error', cleanup);
@@ -81,12 +82,13 @@ const bridge = async (ws: WebSocket, jobId: string) => {
// the terminal share the exact same container (Phase 2).
let boxName: string;
try {
boxName = await acquireUserBox({
const handle = await acquireUserBox({
username: workspace.username,
workdir: workspace.workdir,
containerHome: workspace.containerHome,
});
acquired = true;
handleHolder.current = handle;
boxName = handle.boxName;
} catch (error) {
ws.close(
1011,
@@ -95,7 +97,11 @@ const bridge = async (ws: WebSocket, jobId: string) => {
return;
}
if (isReleased()) return; // client disconnected during startup; cleanup ran
// Cleanup may have run before the awaited handle existed.
if (isReleased()) {
handleHolder.current.release();
return;
}
// Reattach a persistent tmux session across reconnects when available, else a
// plain login shell. `stty` sizes the PTY to the client's viewport up front.