fix(docker): warn when OMNIROUTE_MEMORY_MB disagrees with NODE_OPTIONS heap (#10818)

Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
This commit is contained in:
Ravi Tharuma
2026-08-20 16:48:07 +02:00
committed by GitHub
parent 2f7315882b
commit 56b9d00335
5 changed files with 136 additions and 7 deletions

View File

@@ -49,6 +49,59 @@ export function envHasExplicitHeapFlag(env) {
return String(sourceEnv?.NODE_OPTIONS || "").includes(MAX_OLD_SPACE_FLAG);
}
/** Last `--max-old-space-size=` value in NODE_OPTIONS, or null if absent. */
export function parseNodeOptionsHeapMb(nodeOptions) {
const matches = [...String(nodeOptions || "").matchAll(/--max-old-space-size=(\d+)/g)];
if (matches.length === 0) return null;
const parsed = Number.parseInt(matches[matches.length - 1][1], 10);
return Number.isFinite(parsed) ? parsed : null;
}
/**
* True when OMNIROUTE_MEMORY_MB is an explicit in-range integer (not the
* unset/invalid fallback). Docker images set this; Compose may also set
* NODE_OPTIONS — #10353 needs to know both knobs were intentionally present.
*/
export function envHasExplicitOmnirouteMemoryMb(env) {
const sourceEnv = arguments.length === 0 ? process.env : env;
const parsed = Number.parseInt(String(sourceEnv?.OMNIROUTE_MEMORY_MB ?? ""), 10);
return Number.isFinite(parsed) && parsed >= 64 && parsed <= 16384;
}
/**
* Docker `run-standalone.mjs` appends `--max-old-space-size` from
* OMNIROUTE_MEMORY_MB. V8 last-flag semantics mean that appended value wins
* over an earlier NODE_OPTIONS heap. Warn once when both are set and disagree
* so env dumps stop looking like NODE_OPTIONS is in effect (#10353).
*
* @returns {boolean} true when a warn was emitted
*/
export function warnConflictingHeapLimits(env, omnirouteMb, log = console.warn) {
const nodeMb = parseNodeOptionsHeapMb(env?.NODE_OPTIONS);
if (nodeMb == null || !envHasExplicitOmnirouteMemoryMb(env)) return false;
if (nodeMb === omnirouteMb) return false;
log(
`[omniroute] heap limit conflict: OMNIROUTE_MEMORY_MB=${omnirouteMb} disagrees with NODE_OPTIONS --max-old-space-size=${nodeMb}. ` +
`run-standalone.mjs / Docker appends OMNIROUTE_MEMORY_MB last, so the effective V8 heap is ${omnirouteMb} MB. ` +
`Set only OMNIROUTE_MEMORY_MB (recommended) or make both values match.`
);
return true;
}
/**
* NODE_OPTIONS string for Docker / run-standalone.mjs.
* Explicit OMNIROUTE_MEMORY_MB always appends (wins). Otherwise keep an
* existing NODE_OPTIONS heap flag (#5238). Otherwise append the fallback.
*/
export function buildStandaloneNodeOptions(env = process.env, omnirouteMb) {
const existing = String(env?.NODE_OPTIONS || "").trim();
if (envHasExplicitOmnirouteMemoryMb(env)) {
return `${existing} ${MAX_OLD_SPACE_FLAG}=${omnirouteMb}`.trim();
}
if (existing.includes(MAX_OLD_SPACE_FLAG)) return existing;
return `${existing} ${MAX_OLD_SPACE_FLAG}=${omnirouteMb}`.trim();
}
/**
* Assemble the NODE_OPTIONS string for the spawned server, preserving any flags
* the user already exported. #5238: `omniroute serve` used to UNCONDITIONALLY

View File

@@ -5,6 +5,8 @@ import {
resolveRuntimePorts,
withRuntimePortEnv,
resolveMaxOldSpaceMb,
warnConflictingHeapLimits,
buildStandaloneNodeOptions,
spawnWithForwardedSignals,
} from "../build/runtime-env.mjs";
import { bootstrapEnv } from "../build/bootstrap-env.mjs";
@@ -13,13 +15,13 @@ const env = bootstrapEnv();
const runtimePorts = resolveRuntimePorts(env);
const childEnv = withRuntimePortEnv(env, runtimePorts);
// #2939: honor OMNIROUTE_MEMORY_MB (default 512), the same knob
// `omniroute serve` uses, so Docker users can control the server heap under
// load / large SQLite DBs. A trailing --max-old-space-size wins, so this
// overrides the image fallback without clobbering any other NODE_OPTIONS flags.
// #2939 / #10353: OMNIROUTE_MEMORY_MB is the Docker/standalone heap knob.
// When it is set, we append --max-old-space-size last (V8 last-flag wins).
// When it is unset and NODE_OPTIONS already pins the heap, keep NODE_OPTIONS
// (#5238). Warn when both are set and the numbers disagree.
const maxOldSpaceMb = resolveMaxOldSpaceMb(childEnv.OMNIROUTE_MEMORY_MB);
childEnv.NODE_OPTIONS =
`${childEnv.NODE_OPTIONS || ""} --max-old-space-size=${maxOldSpaceMb}`.trim();
warnConflictingHeapLimits(childEnv, maxOldSpaceMb);
childEnv.NODE_OPTIONS = buildStandaloneNodeOptions(childEnv, maxOldSpaceMb);
// Prefer the WS-aware wrapper (server-ws.mjs) over the bare Next standalone
// server.js: it installs the trusted peer-IP stamp (scripts/dev/peer-stamp.mjs)