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
@@ -0,0 +1,85 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
vi.mock('../../src/env', () => ({ env: { boxIdleMs: 1000 } }));
vi.mock('../../src/runtime/docker', () => ({
ensureUserContainer: vi.fn((args: { username: string }) =>
Promise.resolve(`spoon-box-${args.username}`),
),
stopWorkspaceContainer: vi.fn(() => Promise.resolve()),
userContainerName: (username: string) => `spoon-box-${username}`,
listWorkspaceContainerNames: vi.fn(() => Promise.resolve([])),
}));
const load = async () => await import('../../src/user-container');
describe('box registry', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(async () => {
const module = await load();
module._resetBoxRegistryForTests();
vi.useRealTimers();
vi.resetModules();
vi.clearAllMocks();
});
test('serializes concurrent acquire into a single docker run', async () => {
const module = await load();
const docker = await import('../../src/runtime/docker');
const [a, b] = await Promise.all([
module.acquireUserBox({
username: 'alice',
workdir: '/w',
containerHome: '/home/alice',
}),
module.acquireUserBox({
username: 'alice',
workdir: '/w',
containerHome: '/home/alice',
}),
]);
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
expect(a.boxName).toBe('spoon-box-alice');
a.release();
vi.advanceTimersByTime(2000);
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
b.release();
vi.advanceTimersByTime(2000);
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
});
test('double-release of one handle does not reap a box held elsewhere', async () => {
const module = await load();
const docker = await import('../../src/runtime/docker');
const a = await module.acquireUserBox({
username: 'bob',
workdir: '/w',
containerHome: '/home/bob',
});
const b = await module.acquireUserBox({
username: 'bob',
workdir: '/w',
containerHome: '/home/bob',
});
a.release();
a.release();
vi.advanceTimersByTime(2000);
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
b.release();
vi.advanceTimersByTime(2000);
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
});
test('reconcileExistingBoxes adopts orphans with zero refs and reaps them', async () => {
const module = await load();
const docker = await import('../../src/runtime/docker');
(
docker.listWorkspaceContainerNames as unknown as ReturnType<typeof vi.fn>
).mockResolvedValueOnce(['spoon-box-carol']);
await module.reconcileExistingBoxes();
expect(module.runningBoxUsernames().has('carol')).toBe(true);
vi.advanceTimersByTime(2000);
expect(docker.stopWorkspaceContainer).toHaveBeenCalledWith(
'spoon-box-carol',
);
});
});