mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 08:42:54 +00:00
fix(memory): cap retry sleep delays
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { MAX_SAFE_TIMEOUT_DELAY_MS } from "../../../gateway-client/src/timeouts.js";
|
||||
import { resolveRetryConfig, retryAsync } from "./retry-utils.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("resolveRetryConfig", () => {
|
||||
const defaults = {
|
||||
attempts: 4,
|
||||
@@ -14,6 +19,16 @@ describe("resolveRetryConfig", () => {
|
||||
expect(resolveRetryConfig(defaults, { attempts: Number.POSITIVE_INFINITY }).attempts).toBe(4);
|
||||
expect(resolveRetryConfig(defaults, { attempts: Number.NaN }).attempts).toBe(4);
|
||||
});
|
||||
|
||||
it("caps oversized retry delays at the timer-safe ceiling", () => {
|
||||
const config = resolveRetryConfig(defaults, {
|
||||
minDelayMs: Number.MAX_SAFE_INTEGER,
|
||||
maxDelayMs: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
expect(config.minDelayMs).toBe(MAX_SAFE_TIMEOUT_DELAY_MS);
|
||||
expect(config.maxDelayMs).toBe(MAX_SAFE_TIMEOUT_DELAY_MS);
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryAsync", () => {
|
||||
@@ -26,4 +41,45 @@ describe("retryAsync", () => {
|
||||
|
||||
expect(run).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("caps legacy numeric retry sleeps at the timer-safe ceiling", async () => {
|
||||
const run = vi
|
||||
.fn<() => Promise<string>>()
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockResolvedValueOnce("ok");
|
||||
const timeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation((callback) => {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>;
|
||||
});
|
||||
|
||||
await expect(retryAsync(run, 2, Number.MAX_SAFE_INTEGER)).resolves.toBe("ok");
|
||||
|
||||
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_SAFE_TIMEOUT_DELAY_MS);
|
||||
});
|
||||
|
||||
it("caps retryAfterMs sleeps at the timer-safe ceiling", async () => {
|
||||
const run = vi
|
||||
.fn<() => Promise<string>>()
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockResolvedValueOnce("ok");
|
||||
const timeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation((callback) => {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>;
|
||||
});
|
||||
|
||||
await expect(
|
||||
retryAsync(run, {
|
||||
attempts: 2,
|
||||
minDelayMs: 0,
|
||||
maxDelayMs: Number.MAX_SAFE_INTEGER,
|
||||
retryAfterMs: () => Number.MAX_SAFE_INTEGER,
|
||||
}),
|
||||
).resolves.toBe("ok");
|
||||
|
||||
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_SAFE_TIMEOUT_DELAY_MS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveSafeTimeoutDelayMs } from "../../../gateway-client/src/timeouts.js";
|
||||
|
||||
export type RetryConfig = {
|
||||
attempts?: number;
|
||||
minDelayMs?: number;
|
||||
@@ -60,13 +62,16 @@ export function resolveRetryConfig(
|
||||
overrides?: RetryConfig,
|
||||
): Required<RetryConfig> {
|
||||
const attempts = resolveAttempts(overrides?.attempts, defaults.attempts);
|
||||
const minDelayMs = Math.max(
|
||||
0,
|
||||
const minDelayMs = resolveSafeTimeoutDelayMs(
|
||||
Math.round(clampNumber(overrides?.minDelayMs, defaults.minDelayMs, 0)),
|
||||
{ minMs: 0 },
|
||||
);
|
||||
const maxDelayMs = Math.max(
|
||||
minDelayMs,
|
||||
Math.round(clampNumber(overrides?.maxDelayMs, defaults.maxDelayMs, 0)),
|
||||
resolveSafeTimeoutDelayMs(
|
||||
Math.round(clampNumber(overrides?.maxDelayMs, defaults.maxDelayMs, 0)),
|
||||
{ minMs: 0 },
|
||||
),
|
||||
);
|
||||
const jitter = clampNumber(overrides?.jitter, defaults.jitter, 0, 1);
|
||||
return { attempts, minDelayMs, maxDelayMs, jitter };
|
||||
@@ -96,7 +101,7 @@ export async function retryAsync<T>(
|
||||
if (i === attempts - 1) {
|
||||
break;
|
||||
}
|
||||
await sleep(initialDelayMs * 2 ** i);
|
||||
await sleep(resolveSafeTimeoutDelayMs(initialDelayMs * 2 ** i, { minMs: 0 }));
|
||||
}
|
||||
}
|
||||
throw lastErr ?? new Error("Retry failed");
|
||||
@@ -125,8 +130,8 @@ export async function retryAsync<T>(
|
||||
const retryAfterMs = options.retryAfterMs?.(err);
|
||||
const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs);
|
||||
const baseDelay = hasRetryAfter
|
||||
? Math.max(retryAfterMs, minDelayMs)
|
||||
: minDelayMs * 2 ** (attempt - 1);
|
||||
? Math.max(resolveSafeTimeoutDelayMs(retryAfterMs, { minMs: 0 }), minDelayMs)
|
||||
: resolveSafeTimeoutDelayMs(minDelayMs * 2 ** (attempt - 1), { minMs: 0 });
|
||||
let delay = Math.min(baseDelay, maxDelayMs);
|
||||
delay = applyJitter(delay, resolved.jitter);
|
||||
delay = Math.min(Math.max(delay, minDelayMs), maxDelayMs);
|
||||
|
||||
Reference in New Issue
Block a user