fix(worker): serialize box failure and idle reaping
This commit is contained in:
@@ -38,8 +38,14 @@ const scheduleReapIfIdle = (username: string) => {
|
|||||||
if (!box || box.refs.size > 0) return;
|
if (!box || box.refs.size > 0) return;
|
||||||
if (box.idleTimer) clearTimeout(box.idleTimer);
|
if (box.idleTimer) clearTimeout(box.idleTimer);
|
||||||
box.idleTimer = setTimeout(() => {
|
box.idleTimer = setTimeout(() => {
|
||||||
void stopWorkspaceContainer(userContainerName(username));
|
void withLock(username, async () => {
|
||||||
boxes.delete(username);
|
const current = boxes.get(username);
|
||||||
|
if (current !== box || current.refs.size > 0) return;
|
||||||
|
await stopWorkspaceContainer(current.name);
|
||||||
|
if (boxes.get(username) === current && current.refs.size === 0) {
|
||||||
|
boxes.delete(username);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, env.boxIdleMs);
|
}, env.boxIdleMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +89,10 @@ export const acquireUserBox = (args: {
|
|||||||
return handle;
|
return handle;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handle.release();
|
handle.release();
|
||||||
|
if (boxes.get(args.username) === box && box.refs.size === 0) {
|
||||||
|
if (box.idleTimer) clearTimeout(box.idleTimer);
|
||||||
|
boxes.delete(args.username);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ describe('box registry', () => {
|
|||||||
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
|
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
|
||||||
expect(a.boxName).toBe('spoon-box-alice');
|
expect(a.boxName).toBe('spoon-box-alice');
|
||||||
a.release();
|
a.release();
|
||||||
vi.advanceTimersByTime(2000);
|
await vi.advanceTimersByTimeAsync(2000);
|
||||||
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
|
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
|
||||||
b.release();
|
b.release();
|
||||||
vi.advanceTimersByTime(2000);
|
await vi.advanceTimersByTimeAsync(2000);
|
||||||
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
|
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,10 +62,10 @@ describe('box registry', () => {
|
|||||||
});
|
});
|
||||||
a.release();
|
a.release();
|
||||||
a.release();
|
a.release();
|
||||||
vi.advanceTimersByTime(2000);
|
await vi.advanceTimersByTimeAsync(2000);
|
||||||
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
|
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
|
||||||
b.release();
|
b.release();
|
||||||
vi.advanceTimersByTime(2000);
|
await vi.advanceTimersByTimeAsync(2000);
|
||||||
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
|
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,9 +77,78 @@ describe('box registry', () => {
|
|||||||
).mockResolvedValueOnce(['spoon-box-carol']);
|
).mockResolvedValueOnce(['spoon-box-carol']);
|
||||||
await module.reconcileExistingBoxes();
|
await module.reconcileExistingBoxes();
|
||||||
expect(module.runningBoxUsernames().has('carol')).toBe(true);
|
expect(module.runningBoxUsernames().has('carol')).toBe(true);
|
||||||
vi.advanceTimersByTime(2000);
|
await vi.advanceTimersByTimeAsync(2000);
|
||||||
expect(docker.stopWorkspaceContainer).toHaveBeenCalledWith(
|
expect(docker.stopWorkspaceContainer).toHaveBeenCalledWith(
|
||||||
'spoon-box-carol',
|
'spoon-box-carol',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('retries ensure after the first acquire fails', async () => {
|
||||||
|
const module = await load();
|
||||||
|
const docker = await import('../../src/runtime/docker');
|
||||||
|
const ensure = vi.mocked(docker.ensureUserContainer);
|
||||||
|
ensure
|
||||||
|
.mockRejectedValueOnce(new Error('docker failed'))
|
||||||
|
.mockResolvedValueOnce('spoon-box-dana');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
module.acquireUserBox({
|
||||||
|
username: 'dana',
|
||||||
|
workdir: '/w',
|
||||||
|
containerHome: '/home/dana',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('docker failed');
|
||||||
|
|
||||||
|
const handle = await module.acquireUserBox({
|
||||||
|
username: 'dana',
|
||||||
|
workdir: '/w',
|
||||||
|
containerHome: '/home/dana',
|
||||||
|
});
|
||||||
|
expect(ensure).toHaveBeenCalledTimes(2);
|
||||||
|
expect(handle.boxName).toBe('spoon-box-dana');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finishes an old idle reap before a new acquire can ensure the box', async () => {
|
||||||
|
const module = await load();
|
||||||
|
const docker = await import('../../src/runtime/docker');
|
||||||
|
const ensure = vi.mocked(docker.ensureUserContainer);
|
||||||
|
const stop = vi.mocked(docker.stopWorkspaceContainer);
|
||||||
|
const events: string[] = [];
|
||||||
|
let finishStop!: () => void;
|
||||||
|
const stopDone = new Promise<void>((resolve) => {
|
||||||
|
finishStop = resolve;
|
||||||
|
});
|
||||||
|
ensure.mockReset().mockImplementation((args: { username: string }) => {
|
||||||
|
events.push('ensure');
|
||||||
|
return Promise.resolve(`spoon-box-${args.username}`);
|
||||||
|
});
|
||||||
|
stop.mockReset().mockImplementationOnce(() => {
|
||||||
|
events.push('stop:start');
|
||||||
|
return stopDone.then(() => {
|
||||||
|
events.push('stop:end');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = await module.acquireUserBox({
|
||||||
|
username: 'erin',
|
||||||
|
workdir: '/w',
|
||||||
|
containerHome: '/home/erin',
|
||||||
|
});
|
||||||
|
first.release();
|
||||||
|
await vi.advanceTimersByTimeAsync(1000);
|
||||||
|
|
||||||
|
const secondPromise = module.acquireUserBox({
|
||||||
|
username: 'erin',
|
||||||
|
workdir: '/w',
|
||||||
|
containerHome: '/home/erin',
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(events).toEqual(['ensure', 'stop:start']);
|
||||||
|
|
||||||
|
finishStop();
|
||||||
|
const second = await secondPromise;
|
||||||
|
expect(second.boxName).toBe('spoon-box-erin');
|
||||||
|
expect(events).toEqual(['ensure', 'stop:start', 'stop:end', 'ensure']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user