Files
2026-07-13 13:39:12 +08:00

82 lines
3.3 KiB
TypeScript

import v8 from "node:v8";
/**
* Compute the V8 heap-pressure shed threshold (MB).
*
* The chat pipeline rejects new requests with a 503 once `heapUsed` exceeds this
* value, to avoid hard "JavaScript heap out of memory" crashes under concurrent
* large-context load. The threshold is derived from the process's *actual* V8
* heap ceiling (`heap_size_limit`, which reflects `--max-old-space-size` when set,
* otherwise Node's RAM-derived default) so it auto-adapts across 1 GB / 2 GB /
* large VPS instead of using a fixed number.
*
* A fixed default was the bug: 200 MB sat *below* the app's ~260 MB working set,
* so the guard rejected every request once the heap warmed up (the v3.8.8
* "resource pressure" outage). We shed at 85% of the ceiling — leaving headroom
* for in-flight requests + GC — with a floor that always clears the runtime
* baseline so a small/undersized heap never rejects all traffic.
*
* @param heapSizeLimitMb `v8.getHeapStatistics().heap_size_limit` expressed in MB
* @param override `HEAP_PRESSURE_THRESHOLD_MB` env value — wins when it
* parses to a positive number; invalid/unset → auto-calibrate
*/
export function computeHeapPressureThresholdMb(
heapSizeLimitMb: number,
override?: string | number | null
): number {
const explicit = Number(override);
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
// Shed at 85% of the heap ceiling, but never below a floor that clears the
// runtime's own ~260 MB baseline (+ margin) so an undersized heap degrades to
// "guard never fires" rather than "guard rejects everything".
const SHED_RATIO = 0.85;
const FLOOR_MB = 400;
return Math.max(Math.round(heapSizeLimitMb * SHED_RATIO), FLOOR_MB);
}
/**
* Heap-pressure threshold (MB) resolved once at module load from the live V8 heap
* ceiling. Read by `open-sse/handlers/chatCore.ts`'s memory-pressure guard.
*/
export const HEAP_PRESSURE_THRESHOLD_MB = computeHeapPressureThresholdMb(
v8.getHeapStatistics().heap_size_limit / (1024 * 1024),
process.env.HEAP_PRESSURE_THRESHOLD_MB
);
const HEAP_PRESSURE_MESSAGE =
"Service temporarily unavailable due to resource pressure. Retry shortly.";
export type HeapPressureGuardResult = {
success: false;
status: 503;
error: string;
response: Response;
};
/**
* Memory-pressure shed guard for the chat pipeline (extracted from chatCore's handleChatCore).
* Returns a ready-to-return 503 result when live heap usage exceeds the shed threshold, else null
* to proceed. The heap figure is logged for INTERNAL telemetry only and is NEVER placed in the
* client-facing response (Hard Rule #12). Behaviour is byte-identical to the previous inline guard.
*/
export function checkHeapPressureGuard(
heapUsedMb: number,
thresholdMb: number = HEAP_PRESSURE_THRESHOLD_MB
): HeapPressureGuardResult | null {
if (heapUsedMb <= thresholdMb) return null;
console.warn(
`[chatCore] heap pressure guard tripped: ${Math.round(heapUsedMb)}MB > ${thresholdMb}MB; returning 503`
);
return {
success: false,
status: 503,
error: HEAP_PRESSURE_MESSAGE,
response: new Response(
JSON.stringify({
error: { message: HEAP_PRESSURE_MESSAGE, type: "server_error", code: "heap_pressure" },
}),
{ status: 503, headers: { "Content-Type": "application/json", "Retry-After": "5" } }
),
};
}