Repository navigation
fix: prevent new scheduled jobs from firing immediately on deploy - #10
Conversation
Schedule::isDue() treated null lastTriggeredAt as a missed run because PHP evaluates null < DateTime to true. New jobs now wait for their first scheduled time instead of firing on deploy.
There was a problem hiding this comment.
Pull request overview
Fixes Schedule::isDue() so that brand-new scheduled jobs (with null lastTriggeredAt) don’t get treated as “missed” and run immediately on deploy; instead they wait until they’re actually due per the cron expression.
Changes:
- Add
nullguards inSchedule::isDue()before comparinglastTriggeredAtto computed dates. - Update and expand test coverage to assert new jobs don’t fire immediately when not due, and still do fire when due.
- Adjust
ScheduleBasedCronJobbehavior tests to reflect the corrected scheduling semantics.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/Cron/Schedule.php |
Prevents null lastTriggeredAt from triggering “missed run” logic; falls back to cron isDue(now) evaluation. |
tests/Cron/ScheduleTest.php |
Moves/extends assertions to cover “new job + not due” and “new job + due” behavior. |
tests/Cron/CronJobImplementationTest.php |
Updates missed-run test to be due “now” and adds coverage ensuring new jobs don’t run when not currently due. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public function testNewJobShouldFireImmediatelyWhenDue(): void | ||
| { | ||
| $now = CarbonImmutable::now(); | ||
|
|
||
| // A job that IS due right now should still fire even with null lastTriggeredAt | ||
| $s = Schedule::everyMinute(); | ||
| $this->assertTrue($s->isDue(null)); | ||
|
|
||
| $s = Schedule::everyDay($now->hour, $now->minute); | ||
| $this->assertTrue($s->isDue(null)); | ||
| } |
There was a problem hiding this comment.
These assertions depend on real wall-clock time while Schedule::isDue() calls Carbon::now()/CarbonImmutable::now() internally. If the test runs across a minute boundary, everyDay($now->hour, $now->minute) (and similar time-matching assertions in this file) can become non-due and make the test flaky. Consider freezing time with CarbonImmutable::setTestNow(...) (and resetting afterwards) around tests that expect exact minute matches.
| public function testItShouldRunIfPreviousRunDateMissed(): void | ||
| { | ||
| $c = new class() extends ScheduleBasedCronJob { | ||
| public function getSchedule(): Schedule | ||
| { | ||
| $now = CarbonImmutable::now(); | ||
| $tenMinutesLater = $now->addMinutes(10); | ||
| return Schedule::everyHour(CarbonImmutable::now()->minute); | ||
| } | ||
|
|
||
| public function run(?DateTimeInterface $lastStartedAt): void | ||
| { | ||
| } | ||
| }; | ||
|
|
||
| // Job is due now but last triggered 2 hours ago — catch-up should fire | ||
| $this->assertTrue($c->shouldRun(CarbonImmutable::now()->subHours(2))); | ||
| } |
There was a problem hiding this comment.
getSchedule() uses CarbonImmutable::now()->minute, but Schedule::isDue() also fetches its own now internally. If execution crosses a minute boundary between those calls, this test can become flaky (schedule minute and evaluation minute differ). Freezing time via CarbonImmutable::setTestNow(...) in the test (and resetting after) would make this deterministic.
Summary
Schedule::isDue()treatednulllastTriggeredAt(new job, nocron_datesentry) as a missed run because PHP evaluatesnull < DateTimetotrueRoot cause
Impact
This bug was latent since the initial commit (June 2022). It went unnoticed because:
shouldRun()that intentionally fires on nullFix
Added
$lastTriggeredAt !== nullguard to both comparison branches. WhenlastTriggeredAtis null (new job),isDue()now falls through toCronExpression::isDue(now)-- which correctly checks if the current time matches the cron schedule.Test plan