fix(cron): honor maxConcurrentRuns in timer loop (openclaw#22413) thanks @Takhoffman

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini (failed on unrelated baseline test: src/memory/qmd-manager.test.ts > throws when sqlite index is busy)

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-02-20 22:31:58 -06:00
committed by GitHub
parent 93c2f20a23
commit 7417c36268
3 changed files with 98 additions and 9 deletions

View File

@@ -38,6 +38,13 @@ type TimedCronRunOutcome = CronRunOutcome &
endedAt: number;
};
function resolveRunConcurrency(state: CronServiceState): number {
const raw = state.deps.cronConfig?.maxConcurrentRuns;
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return 1;
}
return Math.max(1, Math.floor(raw));
}
/**
* Exponential backoff delays (in ms) indexed by consecutive error count.
* After the last entry the delay stays constant.
@@ -236,9 +243,11 @@ export async function onTimer(state: CronServiceState) {
}));
});
const results: TimedCronRunOutcome[] = [];
for (const { id, job } of dueJobs) {
const runDueJob = async (params: {
id: string;
job: CronJob;
}): Promise<TimedCronRunOutcome> => {
const { id, job } = params;
const startedAt = state.deps.nowMs();
job.state.runningAtMs = startedAt;
emit(state, { jobId: job.id, action: "started", runAtMs: startedAt });
@@ -276,27 +285,49 @@ export async function onTimer(state: CronServiceState) {
}
})()
: await executeJobCore(state, job);
results.push({ jobId: id, ...result, startedAt, endedAt: state.deps.nowMs() });
return { jobId: id, ...result, startedAt, endedAt: state.deps.nowMs() };
} catch (err) {
state.deps.log.warn(
{ jobId: id, jobName: job.name, timeoutMs: jobTimeoutMs ?? null },
`cron: job failed: ${String(err)}`,
);
results.push({
return {
jobId: id,
status: "error",
error: String(err),
startedAt,
endedAt: state.deps.nowMs(),
});
};
}
}
};
if (results.length > 0) {
const concurrency = Math.min(resolveRunConcurrency(state), Math.max(1, dueJobs.length));
const results: (TimedCronRunOutcome | undefined)[] = Array.from({ length: dueJobs.length });
let cursor = 0;
const workers = Array.from({ length: concurrency }, async () => {
for (;;) {
const index = cursor++;
if (index >= dueJobs.length) {
return;
}
const due = dueJobs[index];
if (!due) {
return;
}
results[index] = await runDueJob(due);
}
});
await Promise.all(workers);
const completedResults: TimedCronRunOutcome[] = results.filter(
(entry): entry is TimedCronRunOutcome => entry !== undefined,
);
if (completedResults.length > 0) {
await locked(state, async () => {
await ensureLoaded(state, { forceReload: true, skipRecompute: true });
for (const result of results) {
for (const result of completedResults) {
const job = state.store?.jobs.find((j) => j.id === result.jobId);
if (!job) {
continue;