mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 01:21:15 +00:00
fix(gateway): eager-load lifecycle runtime to survive in-place upgrades (#84890)
* fix(gateway): eager-load lifecycle runtime to survive in-place upgrades
After a package-swap update (e.g. via update.run), dist/ chunk hashes
rotate while the gateway is still running. The SIGUSR1 listener's first
dynamic import of the lifecycle runtime module then throws
ERR_MODULE_NOT_FOUND inside its async IIFE, silently rejects, and leaves
restart.ts's emittedRestartToken permanently unconsumed. From that point
every scheduleGatewaySigusr1Restart() — including the one update.run
schedules for itself — returns { coalesced: true } without scheduling
anything, and the gateway never restarts until manually kickstarted.
Fix:
1. Eagerly resolve the lifecycle runtime module as the first statement
of runGatewayLoop, before any signal listener is installed. lifecycle.runtime
is a 36-line re-export hub, so loading it once pulls the entire restart
/ respawn / queue / sentinel / handoff graph into memory, immune to
later disk rotation. If the module is missing at startup, fail fast
with a loud error so the supervisor can recover instead of running
half-broken.
2. Defense in depth: catch SIGUSR1 IIFE rejections and call
markGatewaySigusr1RestartHandled() via the eagerly captured reference,
so a transient listener failure doesn't permanently stick the restart
token.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(changelog): mention lifecycle restart eager load
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/restart: eager-load the lifecycle runtime before in-place upgrade signal handling so package replacement does not deadlock restart imports. (#84890) Thanks @myps6415.
|
||||
- CLI/update: start managed Gateway update handoff helpers from a stable existing directory and tolerate deleted cwd/package roots during macOS LaunchAgent handoff. Fixes #83808. (#83875) Thanks @jason-allen-oneal.
|
||||
- Cron: honor `cron.retry.retryOn: ["network"]` for common network error codes such as `EAI_AGAIN`, `EHOSTUNREACH`, and `ENETUNREACH`.
|
||||
- Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.
|
||||
|
||||
@@ -107,6 +107,20 @@ export async function runGatewayLoop(params: {
|
||||
waitForHealthyChild?: (port: number, pid?: number, host?: string) => Promise<boolean>;
|
||||
}) {
|
||||
let startupStartedAt = Date.now();
|
||||
// Eagerly resolve the lifecycle runtime module before installing signal
|
||||
// listeners. Without this, every subsequent lifecycle path (SIGUSR1,
|
||||
// SIGTERM-with-intent, restart iteration hook, stability bundle writer)
|
||||
// depends on a dynamic import() call. After an in-place package upgrade
|
||||
// (e.g. `npm install -g openclaw@latest` triggered via update.run),
|
||||
// dist/ chunk hashes rotate while the process is still running. The next
|
||||
// SIGUSR1 — including the one update.run schedules for itself — would
|
||||
// hit ERR_MODULE_NOT_FOUND from inside its async IIFE, reject silently,
|
||||
// and leave restart.ts's emittedRestartToken permanently unconsumed.
|
||||
// From that point every scheduleGatewaySigusr1Restart() returns
|
||||
// { coalesced: true } and the gateway never restarts. Priming the loader
|
||||
// here pulls the whole re-export graph (lifecycle.runtime.ts is a 36-line
|
||||
// re-export hub) into memory, immune to later disk rotation.
|
||||
const eagerLifecycleRuntime = await loadGatewayLifecycleRuntimeModule();
|
||||
let lock = await acquireGatewayLock({ port: params.lockPort });
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>> | null = null;
|
||||
let shuttingDown = false;
|
||||
@@ -745,7 +759,20 @@ export async function runGatewayLoop(params: {
|
||||
const restartReason = peekGatewaySigusr1RestartReason();
|
||||
markGatewaySigusr1RestartHandled();
|
||||
request("restart", "SIGUSR1", restartReason);
|
||||
})();
|
||||
})().catch((err) => {
|
||||
// Defense in depth: if anything in the listener body rejects, the
|
||||
// SIGUSR1 emit has already advanced emittedRestartToken but no one
|
||||
// called markGatewaySigusr1RestartHandled. Without unsticking the
|
||||
// token here, every subsequent scheduleGatewaySigusr1Restart() would
|
||||
// silently coalesce into the dead in-flight signal and the gateway
|
||||
// would never restart again until manually kickstarted.
|
||||
gatewayLog.error(`SIGUSR1 handler failed: ${formatErrorMessage(err)}`);
|
||||
try {
|
||||
eagerLifecycleRuntime.markGatewaySigusr1RestartHandled();
|
||||
} catch {
|
||||
// Best-effort: the eager reference itself is the recovery path.
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGTERM", onSigterm);
|
||||
|
||||
Reference in New Issue
Block a user