82 lines
3.3 KiB
TypeScript
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" } }
|
|
),
|
|
};
|
|
}
|