mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(discord): honor abortSignal during gateway READY retry backoff (#108561)
* fix(discord): honor abortSignal during gateway READY retry backoff * fix(discord): avoid __testing alias to satisfy Knip production scan * test(discord): make READY abort proof deterministic Co-authored-by: wangmiao0668000666 <wang.miao86@xydigit.com> * fix(retry): support unref abort sleeps Co-authored-by: wangmiao0668000666 <wang.miao86@xydigit.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
27065a65b7
commit
8090cb1c64
@@ -741,6 +741,52 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("waitForGatewayReady", () => {
|
||||
let waitForGatewayReady: (typeof import("../../test-api.js"))["discordGatewayLifecycleTesting"]["waitForGatewayReady"];
|
||||
|
||||
beforeAll(async () => {
|
||||
waitForGatewayReady = (await import("../../test-api.js")).discordGatewayLifecycleTesting
|
||||
.waitForGatewayReady;
|
||||
});
|
||||
|
||||
it("returns promptly when abortSignal fires during the READY retry backoff", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const gateway = {
|
||||
isConnected: false,
|
||||
connect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
ws: null,
|
||||
};
|
||||
const runtime: RuntimeEnv = {
|
||||
log: () => {},
|
||||
error: () => {},
|
||||
exit: () => {},
|
||||
};
|
||||
|
||||
const readyPromise = waitForGatewayReady({
|
||||
gateway,
|
||||
abortSignal: controller.signal,
|
||||
readyTimeoutMs: 200,
|
||||
runtime,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(gateway.connect).toHaveBeenCalledTimes(1);
|
||||
controller.abort();
|
||||
|
||||
await expect(readyPromise).resolves.toBeUndefined();
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(gateway.connect).toHaveBeenCalledTimes(1);
|
||||
expect(gateway.disconnect).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
if (value instanceof Error) {
|
||||
return value;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createTransportActivityStatusPatch,
|
||||
} from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import { asDateTimestampMs, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { danger, sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
|
||||
@@ -399,10 +399,14 @@ async function waitForGatewayReady(params: {
|
||||
if (params.abortSignal?.aborted) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(resolve, DISCORD_GATEWAY_READY_RETRY_BACKOFF_MS);
|
||||
timeout.unref?.();
|
||||
});
|
||||
try {
|
||||
await sleepWithAbort(DISCORD_GATEWAY_READY_RETRY_BACKOFF_MS, params.abortSignal, {
|
||||
ref: false,
|
||||
});
|
||||
} catch {
|
||||
// Abort is normal lifecycle shutdown; do not enter another reconnect attempt.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,3 +585,7 @@ export async function runDiscordGatewayLifecycle(params: {
|
||||
params.threadBindings.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Test-only surface. Re-exported from the plugin root `test-api.ts` entry so Knip's
|
||||
// production scan sees the consumer; tests import `testing` from `test-api.js`.
|
||||
export const testing = { waitForGatewayReady };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Discord API module exposes the plugin public contract.
|
||||
export { discordPlugin } from "./src/channel.js";
|
||||
export { buildFinalizedDiscordDirectInboundContext } from "./src/monitor/inbound-context.test-helpers.js";
|
||||
export { testing as discordGatewayLifecycleTesting } from "./src/monitor/provider.lifecycle.js";
|
||||
export { testing as discordThreadBindingTesting } from "./src/monitor/thread-bindings.manager.js";
|
||||
export { discordOutbound } from "./src/outbound-adapter.js";
|
||||
|
||||
@@ -59,6 +59,22 @@ describe("RetrySupervisor", () => {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("can unref the scheduled timer", async () => {
|
||||
const controller = new AbortController();
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
try {
|
||||
const sleeper = sleepWithAbort(60_000, controller.signal, { ref: false });
|
||||
const timer = setTimeoutSpy.mock.results.at(-1)?.value as NodeJS.Timeout | undefined;
|
||||
|
||||
expect(timer?.hasRef()).toBe(false);
|
||||
controller.abort();
|
||||
await expect(sleeper).rejects.toMatchObject({ message: "aborted" });
|
||||
} finally {
|
||||
controller.abort();
|
||||
setTimeoutSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryAsync", () => {
|
||||
|
||||
@@ -18,7 +18,11 @@ export function computeBackoffSchedule(scheduleMs: readonly number[], attempt: n
|
||||
return attempt <= 0 ? 0 : (scheduleMs[index] ?? 0);
|
||||
}
|
||||
|
||||
export async function sleepWithAbort(ms: number, abortSignal?: AbortSignal): Promise<void> {
|
||||
export async function sleepWithAbort(
|
||||
ms: number,
|
||||
abortSignal?: AbortSignal,
|
||||
options: { ref?: boolean } = {},
|
||||
): Promise<void> {
|
||||
if (!Number.isFinite(ms) || ms <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -50,6 +54,10 @@ export async function sleepWithAbort(ms: number, abortSignal?: AbortSignal): Pro
|
||||
timer = null;
|
||||
resolve();
|
||||
}, delayMs);
|
||||
// Retry loops can stay abortable without keeping an otherwise idle process alive.
|
||||
if (options.ref === false) {
|
||||
timer.unref?.();
|
||||
}
|
||||
if (abortSignal?.aborted) {
|
||||
onAbort();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user