From 1670b970ee8ccbd7d136ba55d976c5f06367c176 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 17:31:00 -0400 Subject: [PATCH] fix(gateway): clamp lock poll timers --- src/infra/gateway-lock.test.ts | 27 +++++++++++++++++++++++++++ src/infra/gateway-lock.ts | 21 ++++++++++++++++----- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/infra/gateway-lock.test.ts b/src/infra/gateway-lock.test.ts index 03ac44f00a5..bab20e56f46 100644 --- a/src/infra/gateway-lock.test.ts +++ b/src/infra/gateway-lock.test.ts @@ -293,6 +293,33 @@ describe("gateway lock", () => { } }); + it("bounds oversized lock polling intervals by the acquire timeout", async () => { + const env = await makeEnv(); + await writeRecentLockFile(env); + const sleepDelays: number[] = []; + let now = 0; + + await expect( + acquireGatewayLock({ + env, + allowInTests: true, + timeoutMs: 5, + pollIntervalMs: Number.MAX_SAFE_INTEGER, + staleMs: 10_000, + platform: "darwin", + now: () => now, + sleep: async (ms) => { + sleepDelays.push(ms); + now = 10; + }, + lockDir: resolveTestLockDir(), + readProcessCmdline: () => ["/usr/local/bin/openclaw", "gateway", "run"], + }), + ).rejects.toBeInstanceOf(GatewayLockError); + + expect(sleepDelays).toEqual([5]); + }); + it("returns null when multi-gateway override is enabled", async () => { const env = await makeEnv(); const lock = await acquireGatewayLock({ diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index 7c5cf36ea67..d58263d15b7 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -4,7 +4,11 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import net from "node:net"; import path from "node:path"; -import { resolveTimestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; +import { + resolvePositiveTimerTimeoutMs, + resolveTimerTimeoutMs, + resolveTimestampMsToIsoString, +} from "@openclaw/normalization-core/number-coercion"; import { z } from "zod"; import { resolveConfigPath, resolveGatewayLockDir, resolveStateDir } from "../config/paths.js"; import { isPidAlive } from "../shared/pid-alive.js"; @@ -256,9 +260,12 @@ export async function acquireGatewayLock( return null; } - const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; - const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; - const staleMs = opts.staleMs ?? DEFAULT_STALE_MS; + const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, DEFAULT_TIMEOUT_MS, 0); + const pollIntervalMs = resolvePositiveTimerTimeoutMs( + opts.pollIntervalMs, + DEFAULT_POLL_INTERVAL_MS, + ); + const staleMs = resolveTimerTimeoutMs(opts.staleMs, DEFAULT_STALE_MS, 0); const platform = opts.platform ?? process.platform; const port = opts.port; const now = opts.now ?? Date.now; @@ -336,7 +343,11 @@ export async function acquireGatewayLock( } } - await sleep(pollIntervalMs); + const remainingMs = timeoutMs - (now() - startedAt); + if (remainingMs <= 0) { + break; + } + await sleep(Math.min(pollIntervalMs, remainingMs)); } }