mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 16:18:26 +00:00
fix(cron): enforce timeout for manual cron runs
This commit is contained in:
@@ -732,6 +732,54 @@ describe("Cron issue regressions", () => {
|
||||
expect(job?.state.lastError).toContain("timed out");
|
||||
});
|
||||
|
||||
it("applies timeoutSeconds to manual cron.run isolated executions", async () => {
|
||||
vi.useRealTimers();
|
||||
const store = await makeStorePath();
|
||||
let observedAbortSignal: AbortSignal | undefined;
|
||||
|
||||
const cron = await startCronForStore({
|
||||
storePath: store.storePath,
|
||||
runIsolatedAgentJob: vi.fn(async ({ abortSignal }) => {
|
||||
observedAbortSignal = abortSignal;
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!abortSignal) {
|
||||
return;
|
||||
}
|
||||
if (abortSignal.aborted) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
||||
});
|
||||
return { status: "ok" as const, summary: "late" };
|
||||
}),
|
||||
});
|
||||
|
||||
const job = await cron.add({
|
||||
name: "manual timeout",
|
||||
enabled: true,
|
||||
schedule: { kind: "every", everyMs: 60_000, anchorMs: Date.now() },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "agentTurn", message: "work", timeoutSeconds: 0.01 },
|
||||
delivery: { mode: "none" },
|
||||
});
|
||||
|
||||
const result = await cron.run(job.id, "force");
|
||||
expect(result).toEqual({ ok: true, ran: true });
|
||||
expect(observedAbortSignal).toBeDefined();
|
||||
expect(observedAbortSignal?.aborted).toBe(true);
|
||||
|
||||
const updated = (await cron.list({ includeDisabled: true })).find(
|
||||
(entry) => entry.id === job.id,
|
||||
);
|
||||
expect(updated?.state.lastStatus).toBe("error");
|
||||
expect(updated?.state.lastError).toContain("timed out");
|
||||
expect(updated?.state.runningAtMs).toBeUndefined();
|
||||
|
||||
cron.stop();
|
||||
});
|
||||
|
||||
it("retries cron schedule computation from the next second when the first attempt returns undefined (#17821)", () => {
|
||||
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
|
||||
const cronJob = createIsolatedRegressionJob({
|
||||
|
||||
@@ -13,6 +13,7 @@ import { locked } from "./locked.js";
|
||||
import type { CronServiceState } from "./state.js";
|
||||
import { ensureLoaded, persist, warnIfDisabled } from "./store.js";
|
||||
import {
|
||||
DEFAULT_JOB_TIMEOUT_MS,
|
||||
applyJobResult,
|
||||
armTimer,
|
||||
emit,
|
||||
@@ -247,8 +248,40 @@ export async function run(state: CronServiceState, id: string, mode?: "due" | "f
|
||||
status: "error";
|
||||
error: string;
|
||||
};
|
||||
const configuredTimeoutMs =
|
||||
prepared.executionJob.payload.kind === "agentTurn" &&
|
||||
typeof prepared.executionJob.payload.timeoutSeconds === "number"
|
||||
? Math.floor(prepared.executionJob.payload.timeoutSeconds * 1_000)
|
||||
: undefined;
|
||||
const jobTimeoutMs =
|
||||
configuredTimeoutMs !== undefined
|
||||
? configuredTimeoutMs <= 0
|
||||
? undefined
|
||||
: configuredTimeoutMs
|
||||
: DEFAULT_JOB_TIMEOUT_MS;
|
||||
try {
|
||||
coreResult = await executeJobCore(state, prepared.executionJob);
|
||||
const runAbortController = typeof jobTimeoutMs === "number" ? new AbortController() : undefined;
|
||||
coreResult =
|
||||
typeof jobTimeoutMs === "number"
|
||||
? await (async () => {
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
executeJobCore(state, prepared.executionJob, runAbortController?.signal),
|
||||
new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
runAbortController?.abort(new Error("cron: job execution timed out"));
|
||||
reject(new Error("cron: job execution timed out"));
|
||||
}, jobTimeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
})()
|
||||
: await executeJobCore(state, prepared.executionJob);
|
||||
} catch (err) {
|
||||
coreResult = { status: "error", error: String(err) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user