fix(preview): zombie process cleanup on anatomy load

This commit is contained in:
2026-06-11 12:05:48 -07:00
parent 2bff945dd8
commit 2036df6c2b

View File

@@ -792,7 +792,60 @@ async function loadPreviews(projectId: string): Promise<Preview[]> {
WHERE project_id = $1 AND state != 'stopped'`,
[projectId],
);
return sortDevPreviewsFrontendFirst(rows).map((r) => ({
// Filter out zombies: if a server is marked 'running' but the URL returns a 50x
// Gateway error or times out, the process died. We mark it stopped so the
// UI can trigger an auto-restart.
const activePreviews: typeof rows = [];
await Promise.all(
rows.map(async (r) => {
if (r.state !== "running") {
activePreviews.push(r);
return;
}
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2500); // Fast 2.5s timeout
const ping = await fetch(r.preview_url, {
method: "HEAD",
signal: controller.signal,
});
clearTimeout(timeout);
// 502/503/504 means Traefik is up but the container isn't answering.
// 404 means Traefik doesn't even know about the route.
if (
ping.status === 502 ||
ping.status === 503 ||
ping.status === 504 ||
ping.status === 404
) {
console.warn(
`[anatomy] Preview zombie detected for ${r.preview_url} (HTTP ${ping.status}). Marking stopped.`,
);
await query(
`UPDATE fs_dev_servers SET state = 'stopped' WHERE id = $1`,
[r.id],
).catch(() => {});
} else {
activePreviews.push(r);
}
} catch (e: any) {
// If the fetch completely fails (e.g. timeout, DNS failure), it's dead.
console.warn(
`[anatomy] Preview zombie detected for ${r.preview_url} (${e.message}). Marking stopped.`,
);
await query(
`UPDATE fs_dev_servers SET state = 'stopped' WHERE id = $1`,
[r.id],
).catch(() => {});
}
}),
);
return sortDevPreviewsFrontendFirst(activePreviews).map((r) => ({
id: r.id,
name: r.name,
command: r.command ?? undefined,