fix(worker): make user-box lifecycle concurrency-safe
This commit is contained in:
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user