mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
perf(cli): skip inactive provider cleanup imports (#121997)
This commit is contained in:
committed by
GitHub
parent
ad22faffe0
commit
452cf09c07
@@ -23,6 +23,7 @@ import {
|
||||
hasLocalServiceProcessExited,
|
||||
stopManagedProviderLocalServices,
|
||||
} from "./provider-local-service.js";
|
||||
import { hasManagedProviderLocalServices } from "./provider-runtime-lifecycle.js";
|
||||
|
||||
const ONE_SHOT_HOST_READY_TIMEOUT_MS = 30_000;
|
||||
const ONE_SHOT_HOST_EXIT_TIMEOUT_MS = 5_000;
|
||||
@@ -204,6 +205,7 @@ describe("provider local service", () => {
|
||||
});
|
||||
|
||||
it("starts an on-demand local service and stops it after idle", async () => {
|
||||
expect(hasManagedProviderLocalServices()).toBe(false);
|
||||
const port = await freePort();
|
||||
const healthUrl = `http://127.0.0.1:${port}/v1/models`;
|
||||
const model = attachModelProviderLocalService(
|
||||
@@ -230,9 +232,11 @@ describe("provider local service", () => {
|
||||
if (!lease) {
|
||||
throw new Error("Expected provider local service lease");
|
||||
}
|
||||
expect(hasManagedProviderLocalServices()).toBe(true);
|
||||
expect((await fetch(healthUrl)).ok).toBe(true);
|
||||
lease.release();
|
||||
await waitForProbeFailure(healthUrl);
|
||||
expect(hasManagedProviderLocalServices()).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves process configuration from the host config", async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
signalChildProcessTree,
|
||||
shouldDetachChildForProcessTree,
|
||||
} from "../process/child-process-tree.js";
|
||||
import { setManagedProviderLocalServicesActive } from "./provider-runtime-lifecycle.js";
|
||||
import { unwrapHeadersInitSentinelsForProviderEgress } from "./provider-secret-egress.js";
|
||||
|
||||
const log = createSubsystemLogger("provider-local-service");
|
||||
@@ -228,6 +229,7 @@ export async function ensureProviderLocalService(
|
||||
installExitHandler();
|
||||
const managed = services.get(key) ?? { active: 0 };
|
||||
services.set(key, managed);
|
||||
setManagedProviderLocalServicesActive(true);
|
||||
clearIdleTimer(managed);
|
||||
managed.active += 1;
|
||||
|
||||
@@ -294,6 +296,7 @@ export function stopManagedProviderLocalServices(): void {
|
||||
stopManagedService(key, managed, "host-shutdown");
|
||||
}
|
||||
services.clear();
|
||||
setManagedProviderLocalServicesActive(false);
|
||||
}
|
||||
|
||||
/** Return bounded local-service state for focused lifecycle tests. */
|
||||
@@ -312,11 +315,7 @@ function validateLocalServiceConfig(service: ModelProviderLocalServiceConfig, pr
|
||||
}
|
||||
|
||||
function resolveHealthUrl(service: ModelProviderLocalServiceConfig, baseUrl: string): string {
|
||||
const configured = service.healthUrl?.trim();
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
return `${baseUrl.replace(/\/+$/, "")}/models`;
|
||||
return service.healthUrl?.trim() || `${baseUrl.replace(/\/+$/, "")}/models`;
|
||||
}
|
||||
|
||||
function localServiceKey(
|
||||
@@ -583,6 +582,7 @@ function scheduleIdleStop(
|
||||
if (!managed.process) {
|
||||
if (!managed.starting) {
|
||||
services.delete(key);
|
||||
setManagedProviderLocalServicesActive(services.size > 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -613,6 +613,7 @@ function stopManagedService(key: string, managed: ManagedLocalService, reason: s
|
||||
managed.process = undefined;
|
||||
managed.lastExit = undefined;
|
||||
services.delete(key);
|
||||
setManagedProviderLocalServicesActive(services.size > 0);
|
||||
if (child) {
|
||||
drainLocalServiceOutput(child);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Provider owners record lifecycle facts here so short-lived CLI teardown can
|
||||
// avoid loading heavyweight provider modules when they never created resources.
|
||||
let managedProviderLocalServicesActive = false;
|
||||
let providerTransportDispatcherPoolActive = false;
|
||||
|
||||
export function setManagedProviderLocalServicesActive(active: boolean): void {
|
||||
managedProviderLocalServicesActive = active;
|
||||
}
|
||||
|
||||
export function hasManagedProviderLocalServices(): boolean {
|
||||
return managedProviderLocalServicesActive;
|
||||
}
|
||||
|
||||
export function setProviderTransportDispatcherPoolActive(active: boolean): void {
|
||||
providerTransportDispatcherPoolActive = active;
|
||||
}
|
||||
|
||||
export function hasProviderTransportDispatcherPool(): boolean {
|
||||
return providerTransportDispatcherPoolActive;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PinnedDispatcherPool } from "../infra/net/pinned-dispatcher-pool.js";
|
||||
import { setProviderTransportDispatcherPoolActive } from "./provider-runtime-lifecycle.js";
|
||||
|
||||
const PROVIDER_DISPATCHER_POOL_MAX_ENTRIES = 16;
|
||||
const PROVIDER_DISPATCHER_POOL_IDLE_TTL_MS = 60_000;
|
||||
@@ -7,10 +8,13 @@ let activePool: PinnedDispatcherPool | undefined;
|
||||
|
||||
/** Returns the current process-lifecycle provider dispatcher pool generation. */
|
||||
export function getProviderTransportDispatcherPool(): PinnedDispatcherPool {
|
||||
activePool ??= new PinnedDispatcherPool({
|
||||
maxEntries: PROVIDER_DISPATCHER_POOL_MAX_ENTRIES,
|
||||
idleTtlMs: PROVIDER_DISPATCHER_POOL_IDLE_TTL_MS,
|
||||
});
|
||||
if (!activePool) {
|
||||
activePool = new PinnedDispatcherPool({
|
||||
maxEntries: PROVIDER_DISPATCHER_POOL_MAX_ENTRIES,
|
||||
idleTtlMs: PROVIDER_DISPATCHER_POOL_IDLE_TTL_MS,
|
||||
});
|
||||
setProviderTransportDispatcherPoolActive(true);
|
||||
}
|
||||
return activePool;
|
||||
}
|
||||
|
||||
@@ -21,6 +25,7 @@ export async function closeProviderTransportDispatcherPool(): Promise<void> {
|
||||
await pool.closeAll();
|
||||
if (activePool === pool) {
|
||||
activePool = undefined;
|
||||
setProviderTransportDispatcherPoolActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,9 @@ const closeActiveMemorySearchManagersMock = vi.hoisted(() => vi.fn(async () => {
|
||||
const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const listRegisteredAgentHarnessesMock = vi.hoisted(() => vi.fn((): unknown[] => []));
|
||||
const disposeRegisteredAgentHarnessesMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const hasManagedProviderLocalServicesMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const hasProviderTransportDispatcherPoolMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const providerCleanupModuleImportState = vi.hoisted(() => ({ local: 0, transport: 0 }));
|
||||
const stopManagedProviderLocalServicesMock = vi.hoisted(() => vi.fn());
|
||||
const closeProviderTransportDispatcherPoolMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const getActiveMcpLoopbackRuntimeMock = vi.hoisted(() =>
|
||||
@@ -320,13 +323,20 @@ vi.mock("../agents/harness/registry.js", () => ({
|
||||
disposeRegisteredAgentHarnesses: disposeRegisteredAgentHarnessesMock,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/provider-local-service.js", () => ({
|
||||
stopManagedProviderLocalServices: stopManagedProviderLocalServicesMock,
|
||||
vi.mock("../agents/provider-runtime-lifecycle.js", () => ({
|
||||
hasManagedProviderLocalServices: hasManagedProviderLocalServicesMock,
|
||||
hasProviderTransportDispatcherPool: hasProviderTransportDispatcherPoolMock,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/provider-transport-dispatcher-pool.js", () => ({
|
||||
closeProviderTransportDispatcherPool: closeProviderTransportDispatcherPoolMock,
|
||||
}));
|
||||
vi.mock("../agents/provider-local-service.js", () => {
|
||||
providerCleanupModuleImportState.local += 1;
|
||||
return { stopManagedProviderLocalServices: stopManagedProviderLocalServicesMock };
|
||||
});
|
||||
|
||||
vi.mock("../agents/provider-transport-dispatcher-pool.js", () => {
|
||||
providerCleanupModuleImportState.transport += 1;
|
||||
return { closeProviderTransportDispatcherPool: closeProviderTransportDispatcherPoolMock };
|
||||
});
|
||||
|
||||
vi.mock("../gateway/mcp-http.loopback-runtime.js", () => ({
|
||||
getActiveMcpLoopbackRuntime: getActiveMcpLoopbackRuntimeMock,
|
||||
@@ -570,6 +580,8 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
hasMemoryRuntimeMock.mockReturnValue(false);
|
||||
listRegisteredAgentHarnessesMock.mockReturnValue([]);
|
||||
hasManagedProviderLocalServicesMock.mockReturnValue(false);
|
||||
hasProviderTransportDispatcherPoolMock.mockReturnValue(false);
|
||||
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
|
||||
outputPrecomputedNodesHelpTextMock.mockReturnValue(false);
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
|
||||
@@ -595,6 +607,22 @@ describe("runCli exit behavior", () => {
|
||||
loggingState.forceConsoleToStderr = false;
|
||||
});
|
||||
|
||||
it("does not load inactive provider cleanup modules for cold help", async () => {
|
||||
tryRouteCliMock.mockResolvedValueOnce(false);
|
||||
const parseAsync = vi.fn().mockResolvedValueOnce(undefined);
|
||||
buildProgramMock.mockReturnValueOnce({
|
||||
commands: [{ name: () => "nodes", aliases: () => [] }],
|
||||
parseAsync,
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1" }, async () => {
|
||||
await runCli(["node", "openclaw", "nodes", "--help"]);
|
||||
});
|
||||
|
||||
expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "nodes", "--help"]);
|
||||
expect(providerCleanupModuleImportState).toEqual({ local: 0, transport: 0 });
|
||||
});
|
||||
|
||||
it("does not import dotenv for gateway forms without a workspace file", async () => {
|
||||
existsSyncOverride.value = () => false;
|
||||
expect(dotenvModuleImportState.count).toBe(0);
|
||||
@@ -673,6 +701,8 @@ describe("runCli exit behavior", () => {
|
||||
it("completes asynchronous teardown before returning to the outer entrypoint", async () => {
|
||||
const order: string[] = [];
|
||||
listRegisteredAgentHarnessesMock.mockReturnValueOnce([{ harness: { id: "copilot" } }]);
|
||||
hasManagedProviderLocalServicesMock.mockReturnValueOnce(true);
|
||||
hasProviderTransportDispatcherPoolMock.mockReturnValueOnce(true);
|
||||
disposeRegisteredAgentHarnessesMock.mockImplementationOnce(async () => {
|
||||
order.push("harnesses");
|
||||
});
|
||||
|
||||
+14
-6
@@ -274,14 +274,22 @@ async function closeCliResources(): Promise<void> {
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
const { stopManagedProviderLocalServices } =
|
||||
await import("../agents/provider-local-service.js");
|
||||
stopManagedProviderLocalServices();
|
||||
const { hasManagedProviderLocalServices } =
|
||||
await import("../agents/provider-runtime-lifecycle.js");
|
||||
if (hasManagedProviderLocalServices()) {
|
||||
const { stopManagedProviderLocalServices } =
|
||||
await import("../agents/provider-local-service.js");
|
||||
stopManagedProviderLocalServices();
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
const { closeProviderTransportDispatcherPool } =
|
||||
await import("../agents/provider-transport-dispatcher-pool.js");
|
||||
await closeProviderTransportDispatcherPool();
|
||||
const { hasProviderTransportDispatcherPool } =
|
||||
await import("../agents/provider-runtime-lifecycle.js");
|
||||
if (hasProviderTransportDispatcherPool()) {
|
||||
const { closeProviderTransportDispatcherPool } =
|
||||
await import("../agents/provider-transport-dispatcher-pool.js");
|
||||
await closeProviderTransportDispatcherPool();
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
const { getActiveMcpLoopbackRuntime } =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Dispatcher } from "undici";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { hasProviderTransportDispatcherPool } from "../../agents/provider-runtime-lifecycle.js";
|
||||
import {
|
||||
closeProviderTransportDispatcherPool,
|
||||
getProviderTransportDispatcherPool,
|
||||
@@ -21,6 +22,14 @@ describe("PinnedDispatcherPool", () => {
|
||||
await closeProviderTransportDispatcherPool();
|
||||
});
|
||||
|
||||
it("records dispatcher-pool activity only for an allocated generation", async () => {
|
||||
expect(hasProviderTransportDispatcherPool()).toBe(false);
|
||||
getProviderTransportDispatcherPool();
|
||||
expect(hasProviderTransportDispatcherPool()).toBe(true);
|
||||
await closeProviderTransportDispatcherPool();
|
||||
expect(hasProviderTransportDispatcherPool()).toBe(false);
|
||||
});
|
||||
|
||||
it("reuses an exact live key and closes it only at lifecycle shutdown", async () => {
|
||||
const pool = new PinnedDispatcherPool({ maxEntries: 2, idleTtlMs: 60_000 });
|
||||
const created = createDispatcher();
|
||||
|
||||
Reference in New Issue
Block a user