mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:14:31 +00:00
fix(discord): unify reconnect watchdog and land #31025/#30530
Landed follow-up intent from contributor PR #31025 (@theotarr) and PR #30530 (@liuxiaopai-ai). Co-authored-by: theotarr <theotarr@users.noreply.github.com> Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
This commit is contained in:
74
src/channels/transport/stall-watchdog.test.ts
Normal file
74
src/channels/transport/stall-watchdog.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createArmableStallWatchdog } from "./stall-watchdog.js";
|
||||
|
||||
describe("createArmableStallWatchdog", () => {
|
||||
it("fires onTimeout once when armed and idle exceeds timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onTimeout = vi.fn();
|
||||
const watchdog = createArmableStallWatchdog({
|
||||
label: "test-watchdog",
|
||||
timeoutMs: 1_000,
|
||||
checkIntervalMs: 100,
|
||||
onTimeout,
|
||||
});
|
||||
|
||||
watchdog.arm();
|
||||
await vi.advanceTimersByTimeAsync(1_500);
|
||||
|
||||
expect(onTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(watchdog.isArmed()).toBe(false);
|
||||
watchdog.stop();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not fire when disarmed before timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onTimeout = vi.fn();
|
||||
const watchdog = createArmableStallWatchdog({
|
||||
label: "test-watchdog",
|
||||
timeoutMs: 1_000,
|
||||
checkIntervalMs: 100,
|
||||
onTimeout,
|
||||
});
|
||||
|
||||
watchdog.arm();
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
watchdog.disarm();
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
|
||||
expect(onTimeout).not.toHaveBeenCalled();
|
||||
watchdog.stop();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("extends timeout window when touched", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onTimeout = vi.fn();
|
||||
const watchdog = createArmableStallWatchdog({
|
||||
label: "test-watchdog",
|
||||
timeoutMs: 1_000,
|
||||
checkIntervalMs: 100,
|
||||
onTimeout,
|
||||
});
|
||||
|
||||
watchdog.arm();
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
watchdog.touch();
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
expect(onTimeout).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(400);
|
||||
expect(onTimeout).toHaveBeenCalledTimes(1);
|
||||
watchdog.stop();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
103
src/channels/transport/stall-watchdog.ts
Normal file
103
src/channels/transport/stall-watchdog.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
|
||||
export type StallWatchdogTimeoutMeta = {
|
||||
idleMs: number;
|
||||
timeoutMs: number;
|
||||
};
|
||||
|
||||
export type ArmableStallWatchdog = {
|
||||
arm: (atMs?: number) => void;
|
||||
touch: (atMs?: number) => void;
|
||||
disarm: () => void;
|
||||
stop: () => void;
|
||||
isArmed: () => boolean;
|
||||
};
|
||||
|
||||
export function createArmableStallWatchdog(params: {
|
||||
label: string;
|
||||
timeoutMs: number;
|
||||
checkIntervalMs?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
runtime?: RuntimeEnv;
|
||||
onTimeout: (meta: StallWatchdogTimeoutMeta) => void;
|
||||
}): ArmableStallWatchdog {
|
||||
const timeoutMs = Math.max(1, Math.floor(params.timeoutMs));
|
||||
const checkIntervalMs = Math.max(
|
||||
100,
|
||||
Math.floor(params.checkIntervalMs ?? Math.min(5_000, Math.max(250, timeoutMs / 6))),
|
||||
);
|
||||
|
||||
let armed = false;
|
||||
let stopped = false;
|
||||
let lastActivityAt = Date.now();
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const clearTimer = () => {
|
||||
if (!timer) {
|
||||
return;
|
||||
}
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
};
|
||||
|
||||
const disarm = () => {
|
||||
armed = false;
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
stopped = true;
|
||||
disarm();
|
||||
clearTimer();
|
||||
params.abortSignal?.removeEventListener("abort", stop);
|
||||
};
|
||||
|
||||
const arm = (atMs?: number) => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
lastActivityAt = atMs ?? Date.now();
|
||||
armed = true;
|
||||
};
|
||||
|
||||
const touch = (atMs?: number) => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
lastActivityAt = atMs ?? Date.now();
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
if (!armed || stopped) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const idleMs = now - lastActivityAt;
|
||||
if (idleMs < timeoutMs) {
|
||||
return;
|
||||
}
|
||||
disarm();
|
||||
params.runtime?.error?.(
|
||||
`[${params.label}] transport watchdog timeout: idle ${Math.round(idleMs / 1000)}s (limit ${Math.round(timeoutMs / 1000)}s)`,
|
||||
);
|
||||
params.onTimeout({ idleMs, timeoutMs });
|
||||
};
|
||||
|
||||
if (params.abortSignal?.aborted) {
|
||||
stop();
|
||||
} else {
|
||||
params.abortSignal?.addEventListener("abort", stop, { once: true });
|
||||
timer = setInterval(check, checkIntervalMs);
|
||||
timer.unref?.();
|
||||
}
|
||||
|
||||
return {
|
||||
arm,
|
||||
touch,
|
||||
disarm,
|
||||
stop,
|
||||
isArmed: () => armed,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user