mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
refactor(gateway): consolidate lifecycle lazy boundary (#74105)
* refactor(gateway): consolidate lifecycle lazy boundary * test(gateway): cover quoted lifecycle imports
This commit is contained in:
committed by
GitHub
parent
53e0874864
commit
616f24fd49
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Build/Gateway: route restart, shutdown, respawn, diagnostics, command-queue cleanup, and runtime cleanup through one stable gateway lifecycle runtime entry so rebuilt packages do not strand long-running gateways on stale hashed chunks. Carries forward #73964. Thanks @pashpashpash.
|
||||||
- Agents/errors: suppress malformed streaming tool-call JSON fragments before they reach chat surfaces while preserving provider request-validation diagnostics. Fixes #59076; keeps #59080 as duplicate coverage. (#59118) Thanks @singleGanghood.
|
- Agents/errors: suppress malformed streaming tool-call JSON fragments before they reach chat surfaces while preserving provider request-validation diagnostics. Fixes #59076; keeps #59080 as duplicate coverage. (#59118) Thanks @singleGanghood.
|
||||||
- CLI/models: restore provider-filtered `models list --all --provider <id>` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.
|
- CLI/models: restore provider-filtered `models list --all --provider <id>` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.
|
||||||
- CLI/models: move the OpenAI listable catalog into the plugin manifest so `models list --all --provider openai` uses the manifest fast path instead of loading provider runtime normalization hooks. Thanks @shakkernerd.
|
- CLI/models: move the OpenAI listable catalog into the plugin manifest so `models list --all --provider openai` uses the manifest fast path instead of loading provider runtime normalization hooks. Thanks @shakkernerd.
|
||||||
|
|||||||
34
src/cli/gateway-cli/lifecycle.runtime.ts
Normal file
34
src/cli/gateway-cli/lifecycle.runtime.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export {
|
||||||
|
abortEmbeddedPiRun,
|
||||||
|
getActiveEmbeddedRunCount,
|
||||||
|
waitForActiveEmbeddedRuns,
|
||||||
|
} from "../../agents/pi-embedded-runner/runs.js";
|
||||||
|
export { getRuntimeConfig } from "../../config/config.js";
|
||||||
|
export {
|
||||||
|
respawnGatewayProcessForUpdate,
|
||||||
|
restartGatewayProcessWithFreshPid,
|
||||||
|
} from "../../infra/process-respawn.js";
|
||||||
|
export {
|
||||||
|
resolveGatewayRestartDeferralTimeoutMs,
|
||||||
|
consumeGatewayRestartIntentSync,
|
||||||
|
consumeGatewaySigusr1RestartAuthorization,
|
||||||
|
isGatewaySigusr1RestartExternallyAllowed,
|
||||||
|
markGatewaySigusr1RestartHandled,
|
||||||
|
peekGatewaySigusr1RestartReason,
|
||||||
|
resetGatewayRestartStateForInProcessRestart,
|
||||||
|
scheduleGatewaySigusr1Restart,
|
||||||
|
} from "../../infra/restart.js";
|
||||||
|
export { markUpdateRestartSentinelFailure } from "../../infra/restart-sentinel.js";
|
||||||
|
export { detectRespawnSupervisor } from "../../infra/supervisor-markers.js";
|
||||||
|
export { writeDiagnosticStabilityBundleForFailureSync } from "../../logging/diagnostic-stability-bundle.js";
|
||||||
|
export {
|
||||||
|
getActiveBundledRuntimeDepsInstallCount,
|
||||||
|
waitForBundledRuntimeDepsInstallIdle,
|
||||||
|
} from "../../plugins/bundled-runtime-deps-activity.js";
|
||||||
|
export {
|
||||||
|
getActiveTaskCount,
|
||||||
|
markGatewayDraining,
|
||||||
|
resetAllLanes,
|
||||||
|
waitForActiveTasks,
|
||||||
|
} from "../../process/command-queue.js";
|
||||||
|
export { reloadTaskRegistryFromStore } from "../../tasks/runtime-internal.js";
|
||||||
@@ -15,48 +15,12 @@ const UPDATE_RESPAWN_HEALTH_POLL_MS = 200;
|
|||||||
type GatewayRunSignalAction = "stop" | "restart";
|
type GatewayRunSignalAction = "stop" | "restart";
|
||||||
type RestartDrainTimeoutMs = number | undefined;
|
type RestartDrainTimeoutMs = number | undefined;
|
||||||
|
|
||||||
type EmbeddedRunsModule = typeof import("../../agents/pi-embedded-runner/runs.js");
|
type GatewayLifecycleRuntimeModule = typeof import("./lifecycle.runtime.js");
|
||||||
type RuntimeConfigModule = typeof import("../../config/config.js");
|
|
||||||
type ProcessRespawnModule = typeof import("../../infra/process-respawn.js");
|
|
||||||
type RestartSentinelModule = typeof import("../../infra/restart-sentinel.js");
|
|
||||||
type RestartModule = typeof import("../../infra/restart.js");
|
|
||||||
type SupervisorMarkersModule = typeof import("../../infra/supervisor-markers.js");
|
|
||||||
type DiagnosticStabilityBundleModule =
|
|
||||||
typeof import("../../logging/diagnostic-stability-bundle.js");
|
|
||||||
type BundledRuntimeDepsActivityModule =
|
|
||||||
typeof import("../../plugins/bundled-runtime-deps-activity.js");
|
|
||||||
type CommandQueueModule = typeof import("../../process/command-queue.js");
|
|
||||||
type RuntimeInternalModule = typeof import("../../tasks/runtime-internal.js");
|
|
||||||
|
|
||||||
let embeddedRunsModule: Promise<EmbeddedRunsModule> | undefined;
|
let gatewayLifecycleRuntimeModule: Promise<GatewayLifecycleRuntimeModule> | undefined;
|
||||||
let runtimeConfigModule: Promise<RuntimeConfigModule> | undefined;
|
|
||||||
let processRespawnModule: Promise<ProcessRespawnModule> | undefined;
|
|
||||||
let restartSentinelModule: Promise<RestartSentinelModule> | undefined;
|
|
||||||
let restartModule: Promise<RestartModule> | undefined;
|
|
||||||
let supervisorMarkersModule: Promise<SupervisorMarkersModule> | undefined;
|
|
||||||
let diagnosticStabilityBundleModule: Promise<DiagnosticStabilityBundleModule> | undefined;
|
|
||||||
let bundledRuntimeDepsActivityModule: Promise<BundledRuntimeDepsActivityModule> | undefined;
|
|
||||||
let commandQueueModule: Promise<CommandQueueModule> | undefined;
|
|
||||||
let runtimeInternalModule: Promise<RuntimeInternalModule> | undefined;
|
|
||||||
|
|
||||||
const loadEmbeddedRunsModule = () =>
|
const loadGatewayLifecycleRuntimeModule = () =>
|
||||||
(embeddedRunsModule ??= import("../../agents/pi-embedded-runner/runs.js"));
|
(gatewayLifecycleRuntimeModule ??= import("./lifecycle.runtime.js"));
|
||||||
const loadRuntimeConfigModule = () => (runtimeConfigModule ??= import("../../config/config.js"));
|
|
||||||
const loadProcessRespawnModule = () =>
|
|
||||||
(processRespawnModule ??= import("../../infra/process-respawn.js"));
|
|
||||||
const loadRestartSentinelModule = () =>
|
|
||||||
(restartSentinelModule ??= import("../../infra/restart-sentinel.js"));
|
|
||||||
const loadRestartModule = () => (restartModule ??= import("../../infra/restart.js"));
|
|
||||||
const loadSupervisorMarkersModule = () =>
|
|
||||||
(supervisorMarkersModule ??= import("../../infra/supervisor-markers.js"));
|
|
||||||
const loadDiagnosticStabilityBundleModule = () =>
|
|
||||||
(diagnosticStabilityBundleModule ??= import("../../logging/diagnostic-stability-bundle.js"));
|
|
||||||
const loadBundledRuntimeDepsActivityModule = () =>
|
|
||||||
(bundledRuntimeDepsActivityModule ??= import("../../plugins/bundled-runtime-deps-activity.js"));
|
|
||||||
const loadCommandQueueModule = () =>
|
|
||||||
(commandQueueModule ??= import("../../process/command-queue.js"));
|
|
||||||
const loadRuntimeInternalModule = () =>
|
|
||||||
(runtimeInternalModule ??= import("../../tasks/runtime-internal.js"));
|
|
||||||
|
|
||||||
function createRestartIterationHook(onRestart: () => Promise<void> | void): () => Promise<boolean> {
|
function createRestartIterationHook(onRestart: () => Promise<void> | void): () => Promise<boolean> {
|
||||||
let isFirstIteration = true;
|
let isFirstIteration = true;
|
||||||
@@ -137,7 +101,7 @@ export async function runGatewayLoop(params: {
|
|||||||
};
|
};
|
||||||
const writeStabilityBundle = async (reason: string, error?: unknown) => {
|
const writeStabilityBundle = async (reason: string, error?: unknown) => {
|
||||||
const { writeDiagnosticStabilityBundleForFailureSync } =
|
const { writeDiagnosticStabilityBundleForFailureSync } =
|
||||||
await loadDiagnosticStabilityBundleModule();
|
await loadGatewayLifecycleRuntimeModule();
|
||||||
const result = writeDiagnosticStabilityBundleForFailureSync(reason, error);
|
const result = writeDiagnosticStabilityBundleForFailureSync(reason, error);
|
||||||
if ("message" in result) {
|
if ("message" in result) {
|
||||||
gatewayLog.warn(result.message);
|
gatewayLog.warn(result.message);
|
||||||
@@ -165,10 +129,12 @@ export async function runGatewayLoop(params: {
|
|||||||
const handleRestartAfterServerClose = async (restartReason?: string) => {
|
const handleRestartAfterServerClose = async (restartReason?: string) => {
|
||||||
const hadLock = await releaseLockIfHeld();
|
const hadLock = await releaseLockIfHeld();
|
||||||
const isUpdateRestart = restartReason === "update.run";
|
const isUpdateRestart = restartReason === "update.run";
|
||||||
const { respawnGatewayProcessForUpdate, restartGatewayProcessWithFreshPid } =
|
const {
|
||||||
await loadProcessRespawnModule();
|
detectRespawnSupervisor,
|
||||||
const { detectRespawnSupervisor } = await loadSupervisorMarkersModule();
|
markUpdateRestartSentinelFailure,
|
||||||
const { markUpdateRestartSentinelFailure } = await loadRestartSentinelModule();
|
respawnGatewayProcessForUpdate,
|
||||||
|
restartGatewayProcessWithFreshPid,
|
||||||
|
} = await loadGatewayLifecycleRuntimeModule();
|
||||||
|
|
||||||
if (isUpdateRestart) {
|
if (isUpdateRestart) {
|
||||||
const respawn = respawnGatewayProcessForUpdate();
|
const respawn = respawnGatewayProcessForUpdate();
|
||||||
@@ -279,10 +245,8 @@ export async function runGatewayLoop(params: {
|
|||||||
const SHUTDOWN_TIMEOUT_MS = SUPERVISOR_STOP_TIMEOUT_MS - 5_000;
|
const SHUTDOWN_TIMEOUT_MS = SUPERVISOR_STOP_TIMEOUT_MS - 5_000;
|
||||||
const resolveRestartDrainTimeoutMs = async (): Promise<RestartDrainTimeoutMs> => {
|
const resolveRestartDrainTimeoutMs = async (): Promise<RestartDrainTimeoutMs> => {
|
||||||
try {
|
try {
|
||||||
const [{ getRuntimeConfig }, { resolveGatewayRestartDeferralTimeoutMs }] = await Promise.all([
|
const { getRuntimeConfig, resolveGatewayRestartDeferralTimeoutMs } =
|
||||||
loadRuntimeConfigModule(),
|
await loadGatewayLifecycleRuntimeModule();
|
||||||
loadRestartModule(),
|
|
||||||
]);
|
|
||||||
const timeoutMs = getRuntimeConfig().gateway?.reload?.deferralTimeoutMs;
|
const timeoutMs = getRuntimeConfig().gateway?.reload?.deferralTimeoutMs;
|
||||||
return resolveGatewayRestartDeferralTimeoutMs(timeoutMs);
|
return resolveGatewayRestartDeferralTimeoutMs(timeoutMs);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -351,15 +315,16 @@ export async function runGatewayLoop(params: {
|
|||||||
// On restart, wait for in-flight agent turns to finish before
|
// On restart, wait for in-flight agent turns to finish before
|
||||||
// tearing down the server so buffered messages are delivered.
|
// tearing down the server so buffered messages are delivered.
|
||||||
if (isRestart) {
|
if (isRestart) {
|
||||||
const [
|
const {
|
||||||
{ abortEmbeddedPiRun, getActiveEmbeddedRunCount, waitForActiveEmbeddedRuns },
|
abortEmbeddedPiRun,
|
||||||
{ getActiveBundledRuntimeDepsInstallCount, waitForBundledRuntimeDepsInstallIdle },
|
getActiveBundledRuntimeDepsInstallCount,
|
||||||
{ getActiveTaskCount, markGatewayDraining, waitForActiveTasks },
|
getActiveEmbeddedRunCount,
|
||||||
] = await Promise.all([
|
getActiveTaskCount,
|
||||||
loadEmbeddedRunsModule(),
|
markGatewayDraining,
|
||||||
loadBundledRuntimeDepsActivityModule(),
|
waitForActiveEmbeddedRuns,
|
||||||
loadCommandQueueModule(),
|
waitForActiveTasks,
|
||||||
]);
|
waitForBundledRuntimeDepsInstallIdle,
|
||||||
|
} = await loadGatewayLifecycleRuntimeModule();
|
||||||
const createStillPendingDrainLogger = () =>
|
const createStillPendingDrainLogger = () =>
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
gatewayLog.warn(
|
gatewayLog.warn(
|
||||||
@@ -429,7 +394,7 @@ export async function runGatewayLoop(params: {
|
|||||||
const onSigterm = () => {
|
const onSigterm = () => {
|
||||||
gatewayLog.info("signal SIGTERM received");
|
gatewayLog.info("signal SIGTERM received");
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const { consumeGatewayRestartIntentSync } = await loadRestartModule();
|
const { consumeGatewayRestartIntentSync } = await loadGatewayLifecycleRuntimeModule();
|
||||||
request(consumeGatewayRestartIntentSync() ? "restart" : "stop", "SIGTERM");
|
request(consumeGatewayRestartIntentSync() ? "restart" : "stop", "SIGTERM");
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
@@ -446,7 +411,7 @@ export async function runGatewayLoop(params: {
|
|||||||
markGatewaySigusr1RestartHandled,
|
markGatewaySigusr1RestartHandled,
|
||||||
peekGatewaySigusr1RestartReason,
|
peekGatewaySigusr1RestartReason,
|
||||||
scheduleGatewaySigusr1Restart,
|
scheduleGatewaySigusr1Restart,
|
||||||
} = await loadRestartModule();
|
} = await loadGatewayLifecycleRuntimeModule();
|
||||||
const authorized = consumeGatewaySigusr1RestartAuthorization();
|
const authorized = consumeGatewaySigusr1RestartAuthorization();
|
||||||
if (!authorized) {
|
if (!authorized) {
|
||||||
if (!isGatewaySigusr1RestartExternallyAllowed()) {
|
if (!isGatewaySigusr1RestartExternallyAllowed()) {
|
||||||
@@ -482,15 +447,11 @@ export async function runGatewayLoop(params: {
|
|||||||
// new work from draining. The same boundary also discards stale restart
|
// new work from draining. The same boundary also discards stale restart
|
||||||
// deferral timers and reloads the task registry from durable state so
|
// deferral timers and reloads the task registry from durable state so
|
||||||
// cancelled/completed work is not kept alive by old in-memory maps.
|
// cancelled/completed work is not kept alive by old in-memory maps.
|
||||||
const [
|
const {
|
||||||
{ resetAllLanes },
|
reloadTaskRegistryFromStore,
|
||||||
{ resetGatewayRestartStateForInProcessRestart },
|
resetAllLanes,
|
||||||
{ reloadTaskRegistryFromStore },
|
resetGatewayRestartStateForInProcessRestart,
|
||||||
] = await Promise.all([
|
} = await loadGatewayLifecycleRuntimeModule();
|
||||||
loadCommandQueueModule(),
|
|
||||||
loadRestartModule(),
|
|
||||||
loadRuntimeInternalModule(),
|
|
||||||
]);
|
|
||||||
resetAllLanes();
|
resetAllLanes();
|
||||||
resetGatewayRestartStateForInProcessRestart();
|
resetGatewayRestartStateForInProcessRestart();
|
||||||
reloadTaskRegistryFromStore();
|
reloadTaskRegistryFromStore();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
import { bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
|
import { bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import tsdownConfig from "../../tsdown.config.ts";
|
import tsdownConfig from "../../tsdown.config.ts";
|
||||||
@@ -41,6 +42,13 @@ function entryKeys(config: TsdownConfigEntry): string[] {
|
|||||||
return Object.keys(config.entry);
|
return Object.keys(config.entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function entrySources(config: TsdownConfigEntry): Record<string, string> {
|
||||||
|
if (!config.entry || Array.isArray(config.entry)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return config.entry;
|
||||||
|
}
|
||||||
|
|
||||||
function hasBundledPluginRuntimeEntry(config: TsdownConfigEntry): boolean {
|
function hasBundledPluginRuntimeEntry(config: TsdownConfigEntry): boolean {
|
||||||
const keys = entryKeys(config);
|
const keys = entryKeys(config);
|
||||||
return keys.includes("index") || keys.includes("runtime-api");
|
return keys.includes("index") || keys.includes("runtime-api");
|
||||||
@@ -56,6 +64,10 @@ function unifiedDistGraph(): TsdownConfigEntry | undefined {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readGatewayRunLoopSource(): string {
|
||||||
|
return readFileSync(new URL("../cli/gateway-cli/run-loop.ts", import.meta.url), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
describe("tsdown config", () => {
|
describe("tsdown config", () => {
|
||||||
it("keeps core, plugin runtime, plugin-sdk, bundled root plugins, and bundled hooks in one dist graph", () => {
|
it("keeps core, plugin runtime, plugin-sdk, bundled root plugins, and bundled hooks in one dist graph", () => {
|
||||||
const distGraph = unifiedDistGraph();
|
const distGraph = unifiedDistGraph();
|
||||||
@@ -66,6 +78,7 @@ describe("tsdown config", () => {
|
|||||||
"agents/auth-profiles.runtime",
|
"agents/auth-profiles.runtime",
|
||||||
"agents/model-catalog.runtime",
|
"agents/model-catalog.runtime",
|
||||||
"agents/models-config.runtime",
|
"agents/models-config.runtime",
|
||||||
|
"cli/gateway-lifecycle.runtime",
|
||||||
"plugins/memory-state",
|
"plugins/memory-state",
|
||||||
"subagent-registry.runtime",
|
"subagent-registry.runtime",
|
||||||
"task-registry-control.runtime",
|
"task-registry-control.runtime",
|
||||||
@@ -85,6 +98,24 @@ describe("tsdown config", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps gateway lifecycle lazy runtime behind one stable dist entry", () => {
|
||||||
|
const distGraph = unifiedDistGraph();
|
||||||
|
|
||||||
|
expect(entrySources(distGraph as TsdownConfigEntry)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes gateway run-loop lifecycle imports through the stable runtime boundary", () => {
|
||||||
|
const importSpecifiers = [
|
||||||
|
...readGatewayRunLoopSource().matchAll(/import\(["']([^"']+)["']\)/gu),
|
||||||
|
].map((match) => match[1]);
|
||||||
|
|
||||||
|
expect(new Set(importSpecifiers)).toEqual(new Set(["./lifecycle.runtime.js"]));
|
||||||
|
});
|
||||||
|
|
||||||
it("emits staged bundled plugins as separate extension graphs", () => {
|
it("emits staged bundled plugins as separate extension graphs", () => {
|
||||||
const stagedGraphs = asConfigArray(tsdownConfig).filter(
|
const stagedGraphs = asConfigArray(tsdownConfig).filter(
|
||||||
(config) => typeof config.outDir === "string" && config.outDir.startsWith("dist/extensions/"),
|
(config) => typeof config.outDir === "string" && config.outDir.startsWith("dist/extensions/"),
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ function buildCoreDistEntries(): Record<string, string> {
|
|||||||
"agents/auth-profiles.runtime": "src/agents/auth-profiles.runtime.ts",
|
"agents/auth-profiles.runtime": "src/agents/auth-profiles.runtime.ts",
|
||||||
"agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts",
|
"agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts",
|
||||||
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
|
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
|
||||||
|
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
|
||||||
"plugins/memory-state": "src/plugins/memory-state.ts",
|
"plugins/memory-state": "src/plugins/memory-state.ts",
|
||||||
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
|
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
|
||||||
"task-registry-control.runtime": "src/tasks/task-registry-control.runtime.ts",
|
"task-registry-control.runtime": "src/tasks/task-registry-control.runtime.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user