mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 15:31:11 +00:00
fix: schedule nextWakeAtMs for isolated sessionTarget cron jobs (#19541)
* fix(cron): repair isolated next wake scheduling * cron: harden isolated next-wake timestamp guards --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -63,15 +63,22 @@ function computeStaggeredCronNextRunAtMs(job: CronJob, nowMs: number) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isFiniteTimestamp(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function resolveEveryAnchorMs(params: {
|
||||
schedule: { everyMs: number; anchorMs?: number };
|
||||
fallbackAnchorMs: number;
|
||||
}) {
|
||||
const raw = params.schedule.anchorMs;
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||
if (isFiniteTimestamp(raw)) {
|
||||
return Math.max(0, Math.floor(raw));
|
||||
}
|
||||
return Math.max(0, Math.floor(params.fallbackAnchorMs));
|
||||
if (isFiniteTimestamp(params.fallbackAnchorMs)) {
|
||||
return Math.max(0, Math.floor(params.fallbackAnchorMs));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function assertSupportedJobSpec(job: Pick<CronJob, "sessionTarget" | "payload">) {
|
||||
@@ -144,11 +151,13 @@ export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | und
|
||||
return nextFromLastRun;
|
||||
}
|
||||
}
|
||||
const fallbackAnchorMs = isFiniteTimestamp(job.createdAtMs) ? job.createdAtMs : nowMs;
|
||||
const anchorMs = resolveEveryAnchorMs({
|
||||
schedule: job.schedule,
|
||||
fallbackAnchorMs: job.createdAtMs,
|
||||
fallbackAnchorMs,
|
||||
});
|
||||
return computeNextRunAtMs({ ...job.schedule, everyMs, anchorMs }, nowMs);
|
||||
const next = computeNextRunAtMs({ ...job.schedule, everyMs, anchorMs }, nowMs);
|
||||
return isFiniteTimestamp(next) ? next : undefined;
|
||||
}
|
||||
if (job.schedule.kind === "at") {
|
||||
// One-shot jobs stay due until they successfully finish.
|
||||
@@ -167,14 +176,14 @@ export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | und
|
||||
: typeof schedule.at === "string"
|
||||
? parseAbsoluteTimeMs(schedule.at)
|
||||
: null;
|
||||
return atMs !== null ? atMs : undefined;
|
||||
return atMs !== null && Number.isFinite(atMs) ? atMs : undefined;
|
||||
}
|
||||
const next = computeStaggeredCronNextRunAtMs(job, nowMs);
|
||||
if (next === undefined && job.schedule.kind === "cron") {
|
||||
const nextSecondMs = Math.floor(nowMs / 1000) * 1000 + 1000;
|
||||
return computeStaggeredCronNextRunAtMs(job, nextSecondMs);
|
||||
}
|
||||
return next;
|
||||
return isFiniteTimestamp(next) ? next : undefined;
|
||||
}
|
||||
|
||||
/** Maximum consecutive schedule errors before auto-disabling a job. */
|
||||
@@ -233,6 +242,11 @@ function normalizeJobTickState(params: { state: CronServiceState; job: CronJob;
|
||||
return { changed, skip: true };
|
||||
}
|
||||
|
||||
if (!isFiniteTimestamp(job.state.nextRunAtMs) && job.state.nextRunAtMs !== undefined) {
|
||||
job.state.nextRunAtMs = undefined;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const runningAt = job.state.runningAtMs;
|
||||
if (typeof runningAt === "number" && nowMs - runningAt > STUCK_RUN_MS) {
|
||||
state.deps.log.warn(
|
||||
@@ -298,7 +312,7 @@ export function recomputeNextRuns(state: CronServiceState): boolean {
|
||||
// Preserving a still-future nextRunAtMs avoids accidentally advancing
|
||||
// a job that hasn't fired yet (e.g. during restart recovery).
|
||||
const nextRun = job.state.nextRunAtMs;
|
||||
const isDueOrMissing = nextRun === undefined || now >= nextRun;
|
||||
const isDueOrMissing = !isFiniteTimestamp(nextRun) || now >= nextRun;
|
||||
if (isDueOrMissing) {
|
||||
if (recomputeJobNextRunAtMs({ state, job, nowMs: now })) {
|
||||
changed = true;
|
||||
@@ -321,7 +335,7 @@ export function recomputeNextRunsForMaintenance(state: CronServiceState): boolea
|
||||
// Only compute missing nextRunAtMs, do NOT recompute existing ones.
|
||||
// If a job was past-due but not found by findDueJobs, recomputing would
|
||||
// cause it to be silently skipped.
|
||||
if (job.state.nextRunAtMs === undefined) {
|
||||
if (!isFiniteTimestamp(job.state.nextRunAtMs)) {
|
||||
if (recomputeJobNextRunAtMs({ state, job, nowMs: now })) {
|
||||
changed = true;
|
||||
}
|
||||
@@ -332,14 +346,18 @@ export function recomputeNextRunsForMaintenance(state: CronServiceState): boolea
|
||||
|
||||
export function nextWakeAtMs(state: CronServiceState) {
|
||||
const jobs = state.store?.jobs ?? [];
|
||||
const enabled = jobs.filter((j) => j.enabled && typeof j.state.nextRunAtMs === "number");
|
||||
const enabled = jobs.filter((j) => j.enabled && isFiniteTimestamp(j.state.nextRunAtMs));
|
||||
if (enabled.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return enabled.reduce(
|
||||
(min, j) => Math.min(min, j.state.nextRunAtMs as number),
|
||||
enabled[0].state.nextRunAtMs as number,
|
||||
);
|
||||
const first = enabled[0]?.state.nextRunAtMs;
|
||||
if (!isFiniteTimestamp(first)) {
|
||||
return undefined;
|
||||
}
|
||||
return enabled.reduce((min, j) => {
|
||||
const next = j.state.nextRunAtMs;
|
||||
return isFiniteTimestamp(next) ? Math.min(min, next) : min;
|
||||
}, first);
|
||||
}
|
||||
|
||||
export function createJob(state: CronServiceState, input: CronJobCreate): CronJob {
|
||||
|
||||
Reference in New Issue
Block a user