mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 09:51:22 +00:00
fix(cron): prevent recomputeNextRuns from skipping due jobs in onTimer (#9823)
* fix(cron): prevent recomputeNextRuns from skipping due jobs in onTimer ensureLoaded(forceReload) called recomputeNextRuns before runDueJobs, which recalculated nextRunAtMs to a strictly future time. Since setTimeout always fires a few ms late, the due check (now >= nextRunAtMs) always failed and every/cron jobs never executed. Fixes #9788. * docs: add changelog entry for cron timer race fix (#9823) (thanks @pycckuu) --------- Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
This commit is contained in:
@@ -126,7 +126,15 @@ async function getFileMtimeMs(path: string): Promise<number | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureLoaded(state: CronServiceState, opts?: { forceReload?: boolean }) {
|
||||
export async function ensureLoaded(
|
||||
state: CronServiceState,
|
||||
opts?: {
|
||||
forceReload?: boolean;
|
||||
/** Skip recomputing nextRunAtMs after load so the caller can run due
|
||||
* jobs against the persisted values first (see onTimer). */
|
||||
skipRecompute?: boolean;
|
||||
},
|
||||
) {
|
||||
// Fast path: store is already in memory. Other callers (add, list, run, …)
|
||||
// trust the in-memory copy to avoid a stat syscall on every operation.
|
||||
if (state.store && !opts?.forceReload) {
|
||||
@@ -255,8 +263,9 @@ export async function ensureLoaded(state: CronServiceState, opts?: { forceReload
|
||||
state.storeLoadedAtMs = state.deps.nowMs();
|
||||
state.storeFileMtimeMs = fileMtimeMs;
|
||||
|
||||
// Recompute next runs after loading to ensure accuracy
|
||||
recomputeNextRuns(state);
|
||||
if (!opts?.skipRecompute) {
|
||||
recomputeNextRuns(state);
|
||||
}
|
||||
|
||||
if (mutated) {
|
||||
await persist(state);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
||||
import type { CronJob } from "../types.js";
|
||||
import type { CronEvent, CronServiceState } from "./state.js";
|
||||
import { computeJobNextRunAtMs, nextWakeAtMs, resolveJobPayloadTextForMain } from "./jobs.js";
|
||||
import {
|
||||
computeJobNextRunAtMs,
|
||||
nextWakeAtMs,
|
||||
recomputeNextRuns,
|
||||
resolveJobPayloadTextForMain,
|
||||
} from "./jobs.js";
|
||||
import { locked } from "./locked.js";
|
||||
import { ensureLoaded, persist } from "./store.js";
|
||||
|
||||
@@ -36,8 +41,12 @@ export async function onTimer(state: CronServiceState) {
|
||||
state.running = true;
|
||||
try {
|
||||
await locked(state, async () => {
|
||||
await ensureLoaded(state, { forceReload: true });
|
||||
// Reload persisted due-times without recomputing so runDueJobs sees
|
||||
// the original nextRunAtMs values. Recomputing first would advance
|
||||
// every/cron slots past the current tick when the timer fires late (#9788).
|
||||
await ensureLoaded(state, { forceReload: true, skipRecompute: true });
|
||||
await runDueJobs(state);
|
||||
recomputeNextRuns(state);
|
||||
await persist(state);
|
||||
armTimer(state);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user