fix(worker): ensure reconciled boxes before acquire

This commit is contained in:
Gabriel Brown
2026-07-10 17:04:41 -04:00
parent 284f7ea52b
commit 0b5a2c0411
2 changed files with 35 additions and 5 deletions
+11 -5
View File
@@ -13,6 +13,7 @@ export type BoxHandle = { boxName: string; release: () => void };
type Box = { type Box = {
name: string; name: string;
initialized: boolean;
refs: Set<symbol>; refs: Set<symbol>;
idleTimer?: ReturnType<typeof setTimeout>; idleTimer?: ReturnType<typeof setTimeout>;
}; };
@@ -71,21 +72,26 @@ export const acquireUserBox = (args: {
}): Promise<BoxHandle> => }): Promise<BoxHandle> =>
withLock(args.username, async () => { withLock(args.username, async () => {
let box = boxes.get(args.username); let box = boxes.get(args.username);
let needsEnsure = false;
if (box?.idleTimer) { if (box?.idleTimer) {
clearTimeout(box.idleTimer); clearTimeout(box.idleTimer);
box.idleTimer = undefined; box.idleTimer = undefined;
} }
if (!box) { if (!box) {
box = { name: userContainerName(args.username), refs: new Set() }; box = {
name: userContainerName(args.username),
initialized: false,
refs: new Set(),
};
boxes.set(args.username, box); boxes.set(args.username, box);
needsEnsure = true;
} }
// Register the ref before the slow Docker call so a failed acquire can // Register the ref before the slow Docker call so a failed acquire can
// release it without leaking registry state. // release it without leaking registry state.
const handle = makeHandle(args.username, box); const handle = makeHandle(args.username, box);
try { try {
if (needsEnsure) box.name = await ensureUserContainer(args); if (!box.initialized) {
box.name = await ensureUserContainer(args);
box.initialized = true;
}
return handle; return handle;
} catch (error) { } catch (error) {
handle.release(); handle.release();
@@ -103,7 +109,7 @@ export const reconcileExistingBoxes = async (): Promise<void> => {
for (const name of names) { for (const name of names) {
const username = name.replace(/^spoon-box-/, ''); const username = name.replace(/^spoon-box-/, '');
if (boxes.has(username)) continue; if (boxes.has(username)) continue;
const box: Box = { name, refs: new Set() }; const box: Box = { name, initialized: false, refs: new Set() };
boxes.set(username, box); boxes.set(username, box);
scheduleReapIfIdle(username); scheduleReapIfIdle(username);
} }
@@ -83,6 +83,30 @@ describe('box registry', () => {
); );
}); });
test('ensures an adopted box once before returning acquire handles', async () => {
const module = await load();
const docker = await import('../../src/runtime/docker');
vi.mocked(docker.listWorkspaceContainerNames).mockResolvedValueOnce([
'spoon-box-casey',
]);
await module.reconcileExistingBoxes();
const first = await module.acquireUserBox({
username: 'casey',
workdir: '/w',
containerHome: '/home/casey',
});
const second = await module.acquireUserBox({
username: 'casey',
workdir: '/w',
containerHome: '/home/casey',
});
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
expect(first.boxName).toBe('spoon-box-casey');
expect(second.boxName).toBe('spoon-box-casey');
});
test('retries ensure after the first acquire fails', async () => { test('retries ensure after the first acquire fails', async () => {
const module = await load(); const module = await load();
const docker = await import('../../src/runtime/docker'); const docker = await import('../../src/runtime/docker');