Initial commit with translated description

This commit is contained in:
2026-03-29 10:19:19 +08:00
commit 5aa1f324c6
81 changed files with 27526 additions and 0 deletions

127
tests/iostat-leak.test.js Normal file
View File

@@ -0,0 +1,127 @@
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert");
const http = require("http");
const { spawn, execSync } = require("child_process");
const os = require("os");
const path = require("path");
const isLinux = os.platform() === "linux";
/**
* Count running iostat processes using pgrep (avoids self-match issues with ps|grep).
* Returns 0 if pgrep finds no matches (exit code 1).
*/
function countIostatProcesses() {
try {
return parseInt(execSync("pgrep -c iostat", { encoding: "utf8" }).trim(), 10) || 0;
} catch {
return 0; // pgrep exits 1 when no matches
}
}
/**
* Simple HTTP GET helper that returns a promise
*/
function httpGet(url) {
return new Promise((resolve, reject) => {
http
.get(url, (res) => {
let body = "";
res.on("data", (chunk) => (body += chunk));
res.on("end", () =>
resolve({
statusCode: res.statusCode,
headers: res.headers,
body,
}),
);
})
.on("error", reject);
});
}
describe(
"iostat resource leak (#31)",
{ skip: !isLinux && "Linux-only test", timeout: 90000 },
() => {
const TEST_PORT = 10000 + Math.floor(Math.random() * 50000);
let serverProcess;
before(async () => {
// Kill any stale iostat processes from prior runs
try {
execSync("pkill iostat 2>/dev/null", { encoding: "utf8" });
} catch {
// No stale processes — expected
}
// Start the server
serverProcess = spawn(process.execPath, [path.join(__dirname, "..", "lib", "server.js")], {
env: { ...process.env, PORT: String(TEST_PORT) },
stdio: ["pipe", "pipe", "pipe"],
});
// Wait for server to be ready by polling the health endpoint
const maxWait = 15000;
const start = Date.now();
while (Date.now() - start < maxWait) {
try {
await httpGet(`http://localhost:${TEST_PORT}/api/health`);
return; // Server is ready
} catch {
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
throw new Error(`Server did not start within ${maxWait}ms`);
});
after(() => {
if (serverProcess) {
serverProcess.kill("SIGTERM");
serverProcess = null;
}
});
it("does not accumulate iostat processes over multiple vitals refreshes", async () => {
// Vitals refresh every 30s (plus once at startup). Wait long enough for
// at least two cycles, then sample multiple times to catch the peak count.
// With the fix, each iostat exits in ~1s so we should never see more than
// 1 running at a time. Without the fix, each cycle spawns an immortal
// process and the count grows unboundedly.
await new Promise((resolve) => setTimeout(resolve, 35000));
// Sample several times over 5s to get a reliable peak
let peak = 0;
for (let i = 0; i < 5; i++) {
peak = Math.max(peak, countIostatProcesses());
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// At most 2 concurrent: one finishing from a prior cycle, one just started
assert.ok(peak <= 2, `Peak iostat process count was ${peak} — leak detected`);
});
it("leaves no orphaned iostat processes after shutdown", async () => {
// Snapshot before shutdown (other test suites may also have servers running
// that spawn iostat, so we compare relative to this baseline)
const baseline = countIostatProcesses();
// Kill the server
if (serverProcess) {
serverProcess.kill("SIGTERM");
serverProcess = null;
}
// Give processes time to clean up (iostat -d 1 2 takes ~1s, plus timeout margin)
await new Promise((resolve) => setTimeout(resolve, 6000));
const remaining = countIostatProcesses();
assert.ok(
remaining <= baseline,
`iostat count grew after shutdown: ${baseline} before, ${remaining} after`,
);
});
},
);