diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php index 3b119f434d..e687406c66 100644 --- a/system/src/Grav/Common/Scheduler/Job.php +++ b/system/src/Grav/Common/Scheduler/Job.php @@ -200,6 +200,28 @@ public function isDue(DateTime $date = null) return $this->executionTime->isDue($date); } + /** + * Check if the Job should have run previously. + * + * @param DateTime|null $date + * @return bool + */ + public function isOverdue(?DateTime $date = null, ?DateTime $lastRun = null) + { + // If the time elapsed since the creation is inferior to the interval, it's not overdue + if ($this->creationTime > $this->executionTime->getPreviousRunDate($date)) { + return false; + } + // Else, if the job has never run, it's overdue + if (null === $lastRun) { + return true; + } + $date = $date ?? new DateTime('now'); + + // Else if the last run time is inferior to the previous scheduled time, it's overdue + return $lastRun < $this->executionTime->getPreviousRunDate($date); + } + /** * Check if the Job is overlapping. * diff --git a/system/src/Grav/Common/Scheduler/Scheduler.php b/system/src/Grav/Common/Scheduler/Scheduler.php index d3cefb0f36..46e6e703b7 100644 --- a/system/src/Grav/Common/Scheduler/Scheduler.php +++ b/system/src/Grav/Common/Scheduler/Scheduler.php @@ -188,7 +188,7 @@ public function addCommand($command, $args = [], $id = null) * @param DateTime|null $runTime Optional, run at specific moment * @param bool $force force run even if not due */ - public function run(DateTime $runTime = null, $force = false) + public function run(DateTime $runTime = null, $force = false, $overdue = false) { $this->loadSavedJobs(); @@ -199,9 +199,17 @@ public function run(DateTime $runTime = null, $force = false) $runTime = new DateTime('now'); } + if ($overdue) { + $lastRuns = []; + foreach ($this->getJobStates()->content() as $id => $state) { + $timestamp = $state['last-run'] ?? time(); + $lastRuns[$id] = DateTime::createFromFormat('U',$timestamp); + } + } + // Star processing jobs foreach ($alljobs as $job) { - if ($job->isDue($runTime) || $force) { + if ($job->isDue($runTime) || $force || ($overdue && $job->isOverdue($runTime, $lastRuns[$job->getId()] ?? null))) { $job->run(); $this->jobs_run[] = $job; } diff --git a/tests/unit/Grav/Common/Scheduler/JobTest.php b/tests/unit/Grav/Common/Scheduler/JobTest.php new file mode 100644 index 0000000000..f28319688e --- /dev/null +++ b/tests/unit/Grav/Common/Scheduler/JobTest.php @@ -0,0 +1,82 @@ +assertEquals($expected, $job->isOverdue($date, $lastRun)); + } + + public function dataProviderForTestIsOverdue() + { + return [ + 'New Job' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => null, + 'lastRun' => null, + 'expected' => false + ], + 'New Job created 1 hour ago' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => new \DateTime('+1 hour'), + 'lastRun' => null, + 'expected' => true + ], + 'New Job created 1 minute ago' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => new \DateTime('+1 minute'), + 'lastRun' => null, + 'expected' => false + ], + 'New Job created 2 minutes ago' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => new \DateTime('+2 minutes'), + 'lastRun' => null, + 'expected' => true + ], + 'Job created 1 hour ago and last run 1 mn ago' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => new \DateTime('+1 hour'), + 'lastRun' => new \DateTime('+1 minutes'), + 'expected' => true + ], + 'Job created 1 hour ago and last run 30 mn ago' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => new \DateTime('+1 hour'), + 'lastRun' => new \DateTime('+30 minutes'), + 'expected' => true + ], + 'Job created 30 minutes ago and last run 1 hour ago' => [ + 'job' => (new Job('ls'))->at('* * * * *'), + 'date' => new \DateTime('+30 minutes'), + 'lastRun' => new \DateTime('+1 hour'), + 'expected' => false + ], + 'New hourly Job' => [ + 'job' => (new Job('ls'))->at('0 * * * *'), + 'date' => null, + 'lastRun' => null, + 'expected' => false + ], + 'New hourly Job created at 2 hours ago' => [ + 'job' => (new Job('ls'))->at('0 * * * *'), + 'date' => new \DateTime('+2 hours'), + 'lastRun' => null, + 'expected' => true + ], + 'Hourly Job created 1 hour ago and last run 30 mn ago' => [ + 'job' => (new Job('ls'))->at('0 * * * *'), + 'date' => new \DateTime('+1 hour'), + 'lastRun' => new \DateTime('+30 minutes'), + 'expected' => true + ], + ]; + } +} diff --git a/tests/unit/Grav/Common/Scheduler/SchedulerTest.php b/tests/unit/Grav/Common/Scheduler/SchedulerTest.php new file mode 100644 index 0000000000..768373faf1 --- /dev/null +++ b/tests/unit/Grav/Common/Scheduler/SchedulerTest.php @@ -0,0 +1,80 @@ + ['args'=>[], 'command'=>'ls', 'at'=>'0 * * * *'], + ], + [ + 'aze45aze' => ['last-run' => strtotime('2021-01-01 00:00:00')], + ] + ], + [ + new \DateTime('+2 hours'), + [ + 'aze45aze' => ['args'=>[], 'command'=>'ls', 'at'=>'0 * * * *'], + 'zedz5a4eza' => ['args'=>[], 'command'=>'ls', 'at'=>'*/15 * * * *'], + ], + [ + 'aze45aze' => ['last-run' => strtotime('-5 minutes')], + ] + ], + ]; + } + + protected function _before() + { + $this->grav = Fixtures::get('grav')(); + $this->scheduler = new Scheduler(); + $this->statusFilePath = Grav::instance()['locator']->findResource('user-data://scheduler', true, true).'/status.yaml'; + } + + protected function _after() + { + if (file_exists($this->statusFilePath)) { + unlink($this->statusFilePath); + } + } + + /** + * @dataProvider dataProviderForTestIsOverdue + */ + public function testIsOverdue($date, $jobs, $status){ + $file = $this->scheduler->getJobStates(); + $file->save($status); + $this->grav['config']->set('scheduler.custom_jobs', $jobs); + $this->scheduler->run($date, false, true); + $this->assertFileExists($this->statusFilePath); + $this->assertFileIsReadable($this->statusFilePath); + dump(file_get_contents($this->statusFilePath)); + foreach ($jobs as $id => $job) { + $this->assertStringContainsString($id, file_get_contents($this->statusFilePath)); + } + } +}