Skip to content

Commit

Permalink
[10.x] Sub-minute Scheduling (#47279)
Browse files Browse the repository at this point in the history
* Add sub-minute scheduling

* formatting

* Allow sub-minute events to conditionally run throughout the minute

* Fix test failure caused by new mutex cache

* Formatting

* Pass queue from Mailable to SendQueuedMailable job (#47612)

* Pass queue from Mailable to SendQueuedMailable job

Fixes issue where the unserialized job has the wrong queue.

* Pass connection from Mailable to SendQueuedMailable job

* Fix property mismatches in SendQueuedMailable

* order

---------

Co-authored-by: Taylor Otwell <taylor@laravel.com>

* Add sub-minute scheduling

* formatting

* Allow sub-minute events to conditionally run throughout the minute

* Fix test failure caused by new mutex cache

* Formatting

* bail early

---------

Co-authored-by: Taylor Otwell <taylor@laravel.com>
Co-authored-by: Tarvo R <TarvoReinpalu@gmail.com>
  • Loading branch information
3 people committed Jun 30, 2023
1 parent a88e2d3 commit 7c707a1
Show file tree
Hide file tree
Showing 8 changed files with 522 additions and 4 deletions.
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.");
}

$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

0 comments on commit 7c707a1

Please sign in to comment.