Fix memory leak in dev server

This commit is contained in:
2026-10-16 14:25:18 +01:00
parent 3d0771f2d0
commit 5dc783fe97
36 changed files with 642 additions and 406 deletions
+46 -48
View File
@@ -4,9 +4,8 @@ import grabDirNames from "../../utils/grab-dir-names";
import writeErrorFile from "../../functions/write-error-file";
import { existsSync, rmSync } from "fs";
const { BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
let retries = 0;
let timeout;
const MAX_RETRIES = 5;
const RETRY_RESET_AFTER_MS = 10000;
export default function () {
return new Command("dev")
.description("Run development server")
@@ -24,51 +23,50 @@ export default function () {
});
}
async function dev() {
clearTimeout(timeout);
if (retries >= MAX_RETRIES) {
console.error(`Dev server crashed ${MAX_RETRIES} times. Exiting.`);
process.exit(1);
// A loop, not recursion: each restart would otherwise keep the previous
// call (and its subprocess) alive for the supervisor's whole lifetime.
let retries = 0;
while (true) {
if (retries >= MAX_RETRIES) {
console.error(`Dev server crashed ${MAX_RETRIES} times. Exiting.`);
process.exit(1);
}
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts");
const dev_spawn_js_file = path.resolve(__dirname, "dev-spawn.js");
const final_spawn_file = existsSync(dev_spawn_js_file)
? dev_spawn_js_file
: dev_spawn_file;
const spawn_options = {
cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"],
async onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error });
},
env: {
...process.env,
NODE_ENV: "development",
},
};
const started_at = Date.now();
let child_process;
try {
child_process = Bun.spawn(spawn_options);
}
catch (error) {
console.error(`Failed to start dev process:`, error);
retries++;
continue;
}
const exited = await child_process.exited;
// A null exit means the child was killed by a signal (e.g. Ctrl+C
// in the shared terminal), which we must not restart.
if (exited == null) {
return;
}
// Restart on any numeric exit (0 included: the server should never
// exit cleanly). Only consecutive quick crashes count toward the
// limit; a child that ran for a while starts the count over.
retries =
Date.now() - started_at >= RETRY_RESET_AFTER_MS ? 1 : retries + 1;
}
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts");
const dev_spawn_js_file = path.resolve(__dirname, "dev-spawn.js");
const final_spawn_file = existsSync(dev_spawn_js_file)
? dev_spawn_js_file
: dev_spawn_file;
const spawn_options = {
cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"],
async onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error });
},
env: {
...process.env,
NODE_ENV: "development",
},
};
let dev_process;
try {
dev_process = Bun.spawn(spawn_options);
}
catch (error) {
console.error(`Failed to start dev process:`, error);
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
return await dev();
}
const exited = await dev_process.exited;
// Restart on any numeric exit (0 included: the server should never exit
// cleanly). A null exit means the child was killed by a signal (e.g.
// Ctrl+C in the shared terminal), which we must not restart.
if (exited != null) {
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
return await dev();
}
timeout = setTimeout(() => {
retries = 0;
}, 10000);
}
+46 -47
View File
@@ -4,9 +4,8 @@ import writeErrorFile from "../../functions/write-error-file";
import grabDirNames from "../../utils/grab-dir-names";
import { existsSync, rmSync } from "fs";
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
let retries = 0;
let timeout;
const MAX_RETRIES = 5;
const RETRY_RESET_AFTER_MS = 10000;
export default function () {
return new Command("start")
.description("Start production server")
@@ -18,50 +17,50 @@ export default function () {
});
}
async function start() {
clearTimeout(timeout);
if (retries >= MAX_RETRIES) {
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
process.exit(1);
// A loop, not recursion: each restart would otherwise keep the previous
// call (and its subprocess) alive for the supervisor's whole lifetime.
let retries = 0;
while (true) {
if (retries >= MAX_RETRIES) {
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
process.exit(1);
}
const prod_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
const prod_spawn_js_file = path.resolve(__dirname, "prod-spawn.js");
const final_spawn_file = existsSync(prod_spawn_js_file)
? prod_spawn_js_file
: prod_spawn_file;
const spawn_options = {
cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"],
onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error });
},
env: {
...process.env,
NODE_ENV: "production",
},
};
const started_at = Date.now();
let child_process;
try {
child_process = Bun.spawn(spawn_options);
}
catch (error) {
console.error(`Failed to start production process:`, error);
retries++;
continue;
}
const exited = await child_process.exited;
// A null exit means the child was killed by a signal (e.g. Ctrl+C
// in the shared terminal), which we must not restart.
if (exited == null) {
return;
}
// Restart on any numeric exit (0 included: the server should never
// exit cleanly). Only consecutive quick crashes count toward the
// limit; a child that ran for a while starts the count over.
retries =
Date.now() - started_at >= RETRY_RESET_AFTER_MS ? 1 : retries + 1;
}
const prod_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
const prod_spawn_js_file = path.resolve(__dirname, "prod-spawn.js");
const final_spawn_file = existsSync(prod_spawn_js_file)
? prod_spawn_js_file
: prod_spawn_file;
const spawn_options = {
cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"],
onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error });
},
env: {
...process.env,
NODE_ENV: "production",
},
};
let dev_process;
try {
dev_process = Bun.spawn(spawn_options);
}
catch (error) {
console.error(`Failed to start production process:`, error);
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
return await start();
}
const exited = await dev_process.exited;
// Restart on any numeric exit (0 included). A null exit means the child
// was killed by a signal, which we must not restart.
if (exited != null) {
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
return await start();
}
timeout = setTimeout(() => {
retries = 0;
}, 10000);
}