Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Sub-minute Scheduling #47279

Merged
merged 13 commits into from
Jun 30, 2023
39 changes: 39 additions & 0 deletions src/Illuminate/Console/Scheduling/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ class Event
*/
public $expression = '* * * * *';

/**
* How often to repeat the event during a minute.
*
* @var int|null
*/
public $repeatSeconds = null;

/**
* The timezone the date should be evaluated on.
*
Expand Down Expand Up @@ -156,6 +163,15 @@ class Event
*/
public $mutexNameResolver;

/**
* The last time the event was checked for eligibility to run.
*
* Utilized by sub-minute repeated events.
*
* @var \Illuminate\Support\Carbon|null
*/
protected $lastChecked;

/**
* The exit status code of the command.
*
Expand Down Expand Up @@ -221,6 +237,27 @@ public function shouldSkipDueToOverlapping()
return $this->withoutOverlapping && ! $this->mutex->create($this);
}

/**
* Determine if the event has been configured to repeat multiple times per minute.
*
* @return bool
*/
public function isRepeatable()
{
return ! is_null($this->repeatSeconds);
}

/**
* Determine if the event is ready to repeat.
*
* @return bool
*/
public function shouldRepeatNow()
{
return $this->isRepeatable()
&& $this->lastChecked?->diffInSeconds() >= $this->repeatSeconds;
}

/**
* Run the command process.
*
Expand Down Expand Up @@ -370,6 +407,8 @@ public function runsInEnvironment($environment)
*/
public function filtersPass($app)
{
$this->lastChecked = Date::now();

foreach ($this->filters as $callback) {
if (! $app->call($callback)) {
return false;
Expand Down
88 changes: 88 additions & 0 deletions src/Illuminate/Console/Scheduling/ManagesFrequencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Console\Scheduling;

use Illuminate\Support\Carbon;
use InvalidArgumentException;

trait ManagesFrequencies
{
Expand Down Expand Up @@ -69,6 +70,93 @@ private function inTimeInterval($startTime, $endTime)
return fn () => $now->between($startTime, $endTime);
}

/**
* Schedule the event to run every second.
*
* @return $this
*/
public function everySecond()
{
return $this->repeatEvery(1);
}

/**
* Schedule the event to run every two seconds.
*
* @return $this
*/
public function everyTwoSeconds()
{
return $this->repeatEvery(2);
}

/**
* Schedule the event to run every five seconds.
*
* @return $this
*/
public function everyFiveSeconds()
{
return $this->repeatEvery(5);
}

/**
* Schedule the event to run every ten seconds.
*
* @return $this
*/
public function everyTenSeconds()
{
return $this->repeatEvery(10);
}

/**
* Schedule the event to run every fifteen seconds.
*
* @return $this
*/
public function everyFifteenSeconds()
{
return $this->repeatEvery(15);
}

/**
* Schedule the event to run every twenty seconds.
*
* @return $this
*/
public function everyTwentySeconds()
{
return $this->repeatEvery(20);
}

/**
* Schedule the event to run every thirty seconds.
*
* @return $this
*/
public function everyThirtySeconds()
{
return $this->repeatEvery(30);
}

/**
* Schedule the event to run multiple times per minute.
*
* @param int $seconds
* @return $this
*/
protected function repeatEvery($seconds)
{
if (60 % $seconds !== 0) {
throw new InvalidArgumentException("The seconds [$seconds] are not evenly divisible by 60.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this say "60 is not evenly divisible by the seconds [$seconds]" or some other wording.

The current wording implies that seconds needs to be divisible by 60 but that's not what the code does...?

}

$this->repeatSeconds = $seconds;

return $this->everyMinute();
}

/**
* Schedule the event to run every minute.
*
Expand Down
9 changes: 8 additions & 1 deletion src/Illuminate/Console/Scheduling/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ class Schedule
*/
protected $dispatcher;

/**
* The cache of mutex results.
*
* @var array<string, bool>
*/
protected $mutexCache = [];

/**
* Create a new schedule instance.
*
Expand Down Expand Up @@ -299,7 +306,7 @@ public function compileArrayInput($key, $value)
*/
public function serverShouldRun(Event $event, DateTimeInterface $time)
{
return $this->schedulingMutex->create($event, $time);
return $this->mutexCache[$event->mutexName()] ??= $this->schedulingMutex->create($event, $time);
}

/**
Expand Down
58 changes: 58 additions & 0 deletions src/Illuminate/Console/Scheduling/ScheduleInterruptCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Illuminate\Console\Scheduling;

use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\Facades\Date;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(name: 'schedule:interrupt')]
class ScheduleInterruptCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'schedule:interrupt';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Interrupt the current schedule run';

/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;

/**
* Create a new schedule interrupt command.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @return void
*/
public function __construct(Cache $cache)
{
parent::__construct();

$this->cache = $cache;
}

/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->cache->put('illuminate:schedule:interrupt', true, Date::now()->endOfMinute());

$this->components->info('Broadcasting schedule interrupt signal.');
}
}
88 changes: 86 additions & 2 deletions src/Illuminate/Console/Scheduling/ScheduleRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Events\ScheduledTaskSkipped;
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Sleep;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;

Expand Down Expand Up @@ -67,6 +69,13 @@ class ScheduleRunCommand extends Command
*/
protected $handler;

/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;

/**
* The PHP binary used by the command.
*
Expand All @@ -91,19 +100,25 @@ public function __construct()
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Illuminate\Contracts\Debug\ExceptionHandler $handler
* @return void
*/
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
public function handle(Schedule $schedule, Dispatcher $dispatcher, Cache $cache, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->cache = $cache;
$this->handler = $handler;
$this->phpBinary = Application::phpBinary();

$this->clearInterruptSignal();

$this->newLine();

foreach ($this->schedule->dueEvents($this->laravel) as $event) {
$events = $this->schedule->dueEvents($this->laravel);

foreach ($events as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));

Expand All @@ -119,6 +134,10 @@ public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHand
$this->eventsRan = true;
}

if ($events->contains->isRepeatable()) {
$this->repeatEvents($events->filter->isRepeatable());
}

if (! $this->eventsRan) {
$this->components->info('No scheduled commands are ready to run.');
} else {
Expand Down Expand Up @@ -193,4 +212,69 @@ protected function runEvent($event)
]);
}
}

/**
* Run the given repeating events.
*
* @param \Illuminate\Support\Collection<\Illuminate\Console\Scheduling\Event> $events
* @return void
*/
protected function repeatEvents($events)
{
$hasEnteredMaintenanceMode = false;

while (Date::now()->lte($this->startedAt->endOfMinute())) {
foreach ($events as $event) {
if ($this->shouldInterrupt()) {
return;
}

if (! $event->shouldRepeatNow()) {
continue;
}

$hasEnteredMaintenanceMode = $hasEnteredMaintenanceMode || $this->laravel->isDownForMaintenance();

if ($hasEnteredMaintenanceMode && ! $event->runsInMaintenanceMode()) {
continue;
}

if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));

continue;
}

if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}

$this->eventsRan = true;
}

Sleep::usleep(100000);
}
}

/**
* Determine if the schedule run should be interrupted.
*
* @return bool
*/
protected function shouldInterrupt()
{
return $this->cache->get('illuminate:schedule:interrupt', false);
}

/**
* Ensure the interrupt signal is cleared.
*
* @return bool
*/
protected function clearInterruptSignal()
{
$this->cache->forget('illuminate:schedule:interrupt');
}
}
Loading
Loading