From 4742ca7c4a4cadf5df9f6da02aa9ef31ce0b6e29 Mon Sep 17 00:00:00 2001 From: Barry <53959224+cal-gooo@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:35:38 -0400 Subject: [PATCH] fix(cron): use effectiveStatus for recurring job backoff scheduling Missed substitution on line 420: the recurring-job backoff branch still checked result.status instead of effectiveStatus, so a job that delivered successfully (delivered=true) could still have its nextRunAtMs pushed out by error backoff even though all status fields showed ok. Also adds a nextRunAtMs assertion for the delivered=true override case. Co-Authored-By: Claude Sonnet 4.6 --- .../service/timer.apply-job-result.test.ts | 27 +++++++++++++++++++ src/cron/service/timer.ts | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/cron/service/timer.apply-job-result.test.ts b/src/cron/service/timer.apply-job-result.test.ts index e64714a1560..4c3e02d40d5 100644 --- a/src/cron/service/timer.apply-job-result.test.ts +++ b/src/cron/service/timer.apply-job-result.test.ts @@ -137,4 +137,31 @@ describe("applyJobResult – delivered=true overrides error status (#50170)", () expect(job.state.lastRunStatus).toBe("ok"); expect(job.state.consecutiveErrors).toBe(0); }); + + it("does not apply error backoff to nextRunAtMs for recurring job when delivered=true overrides error", () => { + // Use a high-frequency schedule so the backoff (30 s minimum) would + // exceed the natural interval and push nextRunAtMs out if applied. + const schedule = { kind: "every" as const, intervalMs: 5_000 }; + + const jobError = createJob({ schedule }); + const jobOk = createJob({ id: "test-job-2", schedule }); + const stateError = createMockState([jobError]); + const stateOk = createMockState([jobOk]); + + applyJobResult(stateError, jobError, { + ...BASE_RESULT, + status: "error", + error: "Warning: Canvas failed", + delivered: true, + }); + + applyJobResult(stateOk, jobOk, { + ...BASE_RESULT, + status: "ok", + delivered: true, + }); + + // Both should schedule the same natural next run — no backoff on delivered=true. + expect(jobError.state.nextRunAtMs).toBe(jobOk.state.nextRunAtMs); + }); }); diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index b15f1e96673..d18f14060d4 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -417,7 +417,7 @@ export function applyJobResult( ); } } - } else if (result.status === "error" && job.enabled) { + } else if (effectiveStatus === "error" && job.enabled) { // Apply exponential backoff for errored jobs to prevent retry storms. const backoff = errorBackoffMs(job.state.consecutiveErrors ?? 1); let normalNext: number | undefined;