refactor(sandbox): dedupe prune loops

This commit is contained in:
Peter Steinberger 2026-02-15 16:33:57 +00:00
parent d4476c6899
commit 5457f6e7e4
1 changed files with 58 additions and 47 deletions

View File

@ -8,69 +8,80 @@ import {
readRegistry,
removeBrowserRegistryEntry,
removeRegistryEntry,
type SandboxBrowserRegistryEntry,
type SandboxRegistryEntry,
} from "./registry.js";
let lastPruneAtMs = 0;
async function pruneSandboxContainers(cfg: SandboxConfig) {
const now = Date.now();
type PruneableRegistryEntry = Pick<
SandboxRegistryEntry,
"containerName" | "createdAtMs" | "lastUsedAtMs"
>;
function shouldPruneSandboxEntry(cfg: SandboxConfig, now: number, entry: PruneableRegistryEntry) {
const idleHours = cfg.prune.idleHours;
const maxAgeDays = cfg.prune.maxAgeDays;
if (idleHours === 0 && maxAgeDays === 0) {
return false;
}
const idleMs = now - entry.lastUsedAtMs;
const ageMs = now - entry.createdAtMs;
return (
(idleHours > 0 && idleMs > idleHours * 60 * 60 * 1000) ||
(maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1000)
);
}
async function pruneSandboxRegistryEntries<TEntry extends PruneableRegistryEntry>(params: {
cfg: SandboxConfig;
read: () => Promise<{ entries: TEntry[] }>;
remove: (containerName: string) => Promise<void>;
onRemoved?: (entry: TEntry) => Promise<void>;
}) {
const now = Date.now();
if (params.cfg.prune.idleHours === 0 && params.cfg.prune.maxAgeDays === 0) {
return;
}
const registry = await readRegistry();
const registry = await params.read();
for (const entry of registry.entries) {
const idleMs = now - entry.lastUsedAtMs;
const ageMs = now - entry.createdAtMs;
if (
(idleHours > 0 && idleMs > idleHours * 60 * 60 * 1000) ||
(maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1000)
) {
try {
await execDocker(["rm", "-f", entry.containerName], {
allowFailure: true,
});
} catch {
// ignore prune failures
} finally {
await removeRegistryEntry(entry.containerName);
}
if (!shouldPruneSandboxEntry(params.cfg, now, entry)) {
continue;
}
try {
await execDocker(["rm", "-f", entry.containerName], {
allowFailure: true,
});
} catch {
// ignore prune failures
} finally {
await params.remove(entry.containerName);
await params.onRemoved?.(entry);
}
}
}
async function pruneSandboxContainers(cfg: SandboxConfig) {
await pruneSandboxRegistryEntries<SandboxRegistryEntry>({
cfg,
read: readRegistry,
remove: removeRegistryEntry,
});
}
async function pruneSandboxBrowsers(cfg: SandboxConfig) {
const now = Date.now();
const idleHours = cfg.prune.idleHours;
const maxAgeDays = cfg.prune.maxAgeDays;
if (idleHours === 0 && maxAgeDays === 0) {
return;
}
const registry = await readBrowserRegistry();
for (const entry of registry.entries) {
const idleMs = now - entry.lastUsedAtMs;
const ageMs = now - entry.createdAtMs;
if (
(idleHours > 0 && idleMs > idleHours * 60 * 60 * 1000) ||
(maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1000)
) {
try {
await execDocker(["rm", "-f", entry.containerName], {
allowFailure: true,
});
} catch {
// ignore prune failures
} finally {
await removeBrowserRegistryEntry(entry.containerName);
const bridge = BROWSER_BRIDGES.get(entry.sessionKey);
if (bridge?.containerName === entry.containerName) {
await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined);
BROWSER_BRIDGES.delete(entry.sessionKey);
}
await pruneSandboxRegistryEntries<SandboxBrowserRegistryEntry>({
cfg,
read: readBrowserRegistry,
remove: removeBrowserRegistryEntry,
onRemoved: async (entry) => {
const bridge = BROWSER_BRIDGES.get(entry.sessionKey);
if (bridge?.containerName === entry.containerName) {
await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined);
BROWSER_BRIDGES.delete(entry.sessionKey);
}
}
}
},
});
}
export async function maybePruneSandboxes(cfg: SandboxConfig) {