fix(cron): handle invalid schedule reload errors
This commit is contained in:
parent
2efa044a29
commit
d1623edb18
@ -76,4 +76,70 @@ describe("forceReload repairs externally changed cron schedules", () => {
|
||||
const persistedJob = persisted.jobs?.find((job) => job.id === jobId);
|
||||
expect(persistedJob?.state?.nextRunAtMs).toBe(correctedNextRunAtMs);
|
||||
});
|
||||
|
||||
it("records schedule errors instead of aborting reload when an external edit is invalid", async () => {
|
||||
const store = await makeStorePath();
|
||||
const nowMs = Date.parse("2026-03-19T01:44:00.000Z");
|
||||
const jobId = "external-invalid-schedule";
|
||||
|
||||
const createJob = (expr: string): CronJob => ({
|
||||
id: jobId,
|
||||
name: "external invalid schedule",
|
||||
enabled: true,
|
||||
createdAtMs: Date.parse("2026-03-18T00:30:00.000Z"),
|
||||
updatedAtMs: Date.parse("2026-03-19T01:44:00.000Z"),
|
||||
schedule: { kind: "cron", expr, tz: "Asia/Shanghai", staggerMs: 0 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "tick" },
|
||||
state: {
|
||||
nextRunAtMs: Date.parse("2026-03-20T00:30:00.000Z"),
|
||||
lastRunAtMs: Date.parse("2026-03-19T00:30:00.000Z"),
|
||||
lastStatus: "ok",
|
||||
lastRunStatus: "ok",
|
||||
},
|
||||
});
|
||||
|
||||
await writeCronStoreSnapshot({
|
||||
storePath: store.storePath,
|
||||
jobs: [createJob("30 8 * * *")],
|
||||
});
|
||||
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath: store.storePath,
|
||||
log: noopLogger,
|
||||
nowMs: () => nowMs,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeatNow: vi.fn(),
|
||||
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })),
|
||||
});
|
||||
|
||||
await ensureLoaded(state, { skipRecompute: true });
|
||||
|
||||
await writeCronStoreSnapshot({
|
||||
storePath: store.storePath,
|
||||
jobs: [createJob("not a valid cron")],
|
||||
});
|
||||
|
||||
await expect(
|
||||
ensureLoaded(state, { forceReload: true, skipRecompute: true }),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
const reloaded = state.store?.jobs.find((job) => job.id === jobId);
|
||||
expect(reloaded?.state.nextRunAtMs).toBeUndefined();
|
||||
expect(reloaded?.state.scheduleErrorCount).toBe(1);
|
||||
expect(reloaded?.state.lastError).toMatch(/^schedule error:/);
|
||||
|
||||
const persisted = JSON.parse(await fs.readFile(store.storePath, "utf8")) as {
|
||||
jobs?: Array<{
|
||||
id: string;
|
||||
state?: { scheduleErrorCount?: number; lastError?: string; nextRunAtMs?: number };
|
||||
}>;
|
||||
};
|
||||
const persistedJob = persisted.jobs?.find((job) => job.id === jobId);
|
||||
expect(persistedJob?.state?.scheduleErrorCount).toBe(1);
|
||||
expect(persistedJob?.state?.lastError).toMatch(/^schedule error:/);
|
||||
expect(persistedJob?.state?.nextRunAtMs).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import { normalizeStoredCronJobs } from "../store-migration.js";
|
||||
import { loadCronStore, saveCronStore } from "../store.js";
|
||||
import type { CronJob } from "../types.js";
|
||||
import { computeJobNextRunAtMs, recomputeNextRuns } from "./jobs.js";
|
||||
import { computeJobNextRunAtMs, recordScheduleComputeError, recomputeNextRuns } from "./jobs.js";
|
||||
import type { CronServiceState } from "./state.js";
|
||||
|
||||
async function getFileMtimeMs(path: string): Promise<number | null> {
|
||||
@ -55,7 +55,19 @@ function repairNextRunsAfterExternalReload(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextRunAtMs = job.enabled ? computeJobNextRunAtMs(job, now) : undefined;
|
||||
let nextRunAtMs: number | undefined;
|
||||
try {
|
||||
nextRunAtMs = job.enabled ? computeJobNextRunAtMs(job, now) : undefined;
|
||||
if (job.state.scheduleErrorCount !== undefined) {
|
||||
job.state.scheduleErrorCount = undefined;
|
||||
changed = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (recordScheduleComputeError({ state, job, err })) {
|
||||
changed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (job.state.nextRunAtMs !== nextRunAtMs) {
|
||||
job.state.nextRunAtMs = nextRunAtMs;
|
||||
changed = true;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user