fix(scripts): clamp memory fd repro timers

This commit is contained in:
Vincent Koc
2026-06-22 03:24:51 +02:00
parent 2800ce4e28
commit 4113982fa8
2 changed files with 90 additions and 9 deletions

View File

@@ -27,6 +27,7 @@ const ISSUE_FILE_COUNTS = [
const ISSUE_MEMORY_FILE_COUNT = ISSUE_FILE_COUNTS.reduce((sum, [, count]) => sum + count, 0);
const DEFAULT_FILE_COUNT = 512;
const DEFAULT_MAX_WORKSPACE_REG_FDS = process.platform === "darwin" ? 8 : 64;
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
/**
* Maximum gateway-ready output tail retained while waiting for startup.
*/
@@ -134,6 +135,24 @@ function readPositiveNumberEnv(name, fallback) {
return raw == null || raw.trim() === "" ? fallback : readPositiveNumber(raw, name);
}
function clampTimerTimeoutMs(valueMs, minMs = 1) {
const min = Math.max(0, Math.floor(minMs));
const value = Number.isFinite(valueMs) ? valueMs : min;
return Math.min(Math.max(Math.floor(value), min), MAX_TIMER_TIMEOUT_MS);
}
function readTimerTimeoutNumber(value, label, minMs = 1) {
const parsed = minMs > 0 ? readPositiveNumber(value, label) : readNumber(value, label);
return clampTimerTimeoutMs(parsed, minMs);
}
function readTimerTimeoutNumberEnv(name, fallback, minMs = 1) {
const raw = process.env[name];
return raw == null || raw.trim() === ""
? clampTimerTimeoutMs(fallback, minMs)
: readTimerTimeoutNumber(raw, name, minMs);
}
/**
* Parses memory FD repro CLI arguments and environment fallbacks.
*/
@@ -192,13 +211,13 @@ export function parseArgs(argv) {
options.minLeakedFds = readPositiveNumber(readValue(), "--min-leaked-fds");
break;
case "--invoke-timeout-ms":
options.invokeTimeoutMs = readPositiveNumber(readValue(), "--invoke-timeout-ms");
options.invokeTimeoutMs = readTimerTimeoutNumber(readValue(), "--invoke-timeout-ms");
break;
case "--sample-delay-ms":
options.sampleDelayMs = readNumber(readValue(), "--sample-delay-ms");
options.sampleDelayMs = readTimerTimeoutNumber(readValue(), "--sample-delay-ms", 0);
break;
case "--settle-delay-ms":
options.settleDelayMs = readNumber(readValue(), "--settle-delay-ms");
options.settleDelayMs = readTimerTimeoutNumber(readValue(), "--settle-delay-ms", 0);
break;
case "--output-dir":
options.outputDir = path.resolve(readValue());
@@ -222,9 +241,17 @@ export function parseArgs(argv) {
"OPENCLAW_MEMORY_FD_REPRO_MAX_WORKSPACE_REG_FDS",
DEFAULT_MAX_WORKSPACE_REG_FDS,
);
options.invokeTimeoutMs ??= readPositiveNumberEnv("OPENCLAW_MEMORY_FD_REPRO_TIMEOUT_MS", 30_000);
options.sampleDelayMs ??= readNumberEnv("OPENCLAW_MEMORY_FD_REPRO_SAMPLE_DELAY_MS", 1_000);
options.settleDelayMs ??= readNumberEnv("OPENCLAW_MEMORY_FD_REPRO_SETTLE_DELAY_MS", 5_000);
options.invokeTimeoutMs ??= readTimerTimeoutNumberEnv("OPENCLAW_MEMORY_FD_REPRO_TIMEOUT_MS", 30_000);
options.sampleDelayMs ??= readTimerTimeoutNumberEnv(
"OPENCLAW_MEMORY_FD_REPRO_SAMPLE_DELAY_MS",
1_000,
0,
);
options.settleDelayMs ??= readTimerTimeoutNumberEnv(
"OPENCLAW_MEMORY_FD_REPRO_SETTLE_DELAY_MS",
5_000,
0,
);
if (!Number.isFinite(options.fileCount) || options.fileCount <= 0) {
throw new Error("file count must be greater than 0");
}
@@ -243,7 +270,7 @@ function logStep(message) {
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
setTimeout(resolve, clampTimerTimeoutMs(ms, 0));
});
}
@@ -676,9 +703,10 @@ export function classifyMemorySearchInvokeResponse({ httpOk, status, bodyText })
};
}
async function invokeMemorySearch({ port, token, timeoutMs }) {
export async function invokeMemorySearch({ port, token, timeoutMs }) {
const resolvedTimeoutMs = clampTimerTimeoutMs(timeoutMs);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const timer = setTimeout(() => controller.abort(), resolvedTimeoutMs);
const startedAt = Date.now();
try {
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {

View File

@@ -1,8 +1,10 @@
// Check Memory Fd Repro tests cover check memory fd repro script behavior.
import { EventEmitter } from "node:events";
import fs from "node:fs";
import { createServer, type Server } from "node:http";
import os from "node:os";
import path from "node:path";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { describe, expect, it, vi } from "vitest";
import {
GATEWAY_READY_OUTPUT_MAX_CHARS,
@@ -10,6 +12,7 @@ import {
MEMORY_SEARCH_RESPONSE_MAX_BYTES,
classifyMemorySearchInvokeResponse,
hasChildExited,
invokeMemorySearch,
parseArgs,
readBoundedResponseText,
readNumber,
@@ -21,6 +24,21 @@ import {
} from "../../scripts/check-memory-fd-repro.mjs";
import { withEnv } from "../../src/test-utils/env.js";
async function listen(server: Server): Promise<number> {
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.off("error", reject);
resolve();
});
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("test server did not expose a TCP port");
}
return address.port;
}
describe("check-memory-fd-repro", () => {
it("parses file, fd, and timing limits as strict integers", () => {
expect(readNumber("0", "limit")).toBe(0);
@@ -40,13 +58,17 @@ describe("check-memory-fd-repro", () => {
OPENCLAW_MEMORY_FD_REPRO_FILES: "17",
OPENCLAW_MEMORY_FD_REPRO_MAX_WORKSPACE_REG_FDS: "0",
OPENCLAW_MEMORY_FD_REPRO_SAMPLE_DELAY_MS: "0",
OPENCLAW_MEMORY_FD_REPRO_SETTLE_DELAY_MS: String(MAX_TIMER_TIMEOUT_MS + 1),
OPENCLAW_MEMORY_FD_REPRO_TIMEOUT_MS: String(MAX_TIMER_TIMEOUT_MS + 1),
},
() => parseArgs([]),
),
).toMatchObject({
fileCount: 17,
invokeTimeoutMs: MAX_TIMER_TIMEOUT_MS,
maxWorkspaceRegFds: 0,
sampleDelayMs: 0,
settleDelayMs: MAX_TIMER_TIMEOUT_MS,
});
expect(() =>
@@ -109,6 +131,37 @@ describe("check-memory-fd-repro", () => {
});
});
it("clamps oversized memory_search invoke timers before scheduling", async () => {
const server = createServer((_request, response) => {
setTimeout(() => {
response.writeHead(200, { "content-type": "application/json" }).end(
JSON.stringify({
ok: true,
result: {
content: [{ type: "text", text: JSON.stringify({ results: [] }) }],
},
}),
);
}, 25);
});
const port = await listen(server);
try {
await expect(
invokeMemorySearch({
port,
token: "test-token",
timeoutMs: MAX_TIMER_TIMEOUT_MS + 1,
}),
).resolves.toMatchObject({
gatewayOk: true,
ok: true,
resultCount: 0,
});
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it("uses a fast matching probe query instead of a no-hit stress query", () => {
expect(MEMORY_SEARCH_PROBE_QUERY).toBe("Top-level memory file");
expect(MEMORY_SEARCH_PROBE_QUERY).not.toContain("nomatch");