Skip to content

Commit

Permalink
[2.x] Adds scheduler proxy command to normalize sub-minute schedules (#…
Browse files Browse the repository at this point in the history
…167)

* add vapor scheduler proxy

* update syntax

* fix return

* Apply fixes from StyleCI

* adds tests

* remove console events trait

* conditional test

* wip

* wip

* wip

* wip

* Update VaporScheduleCommand.php

---------

Co-authored-by: StyleCI Bot <[email protected]>
Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2023
1 parent eb7b7fa commit 03bc115
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 1 deletion.
98 changes: 98 additions & 0 deletions src/Console/Commands/VaporScheduleCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace Laravel\Vapor\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Str;

class VaporScheduleCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'vapor:schedule';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the scheduled commands at the beginning of every minute';

/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = true;

/**
* Execute the console command.
*/
public function handle(): int
{
if (! $cache = $this->ensureValidCacheDriver()) {
$this->call('schedule:run');

return 0;
}

$key = (string) Str::uuid();
$lockObtained = false;

while (true) {
if (! $lockObtained) {
$lockObtained = $this->obtainLock($cache, $key);
}

if ($lockObtained && now()->second === 0) {
$this->releaseLock($cache);

$this->call('schedule:run');

return 0;
}

if (! $lockObtained && now()->second === 0) {
return 1;
}

usleep(10000);
}
}

/**
* Ensure the cache driver is valid.
*/
protected function ensureValidCacheDriver(): ?Repository
{
$manager = $this->laravel['cache'];

if (in_array($manager->getDefaultDriver(), ['memcached', 'redis', 'dynamodb', 'database'])) {
return $manager->driver();
}

return null;
}

/**
* Obtain the lock for the schedule.
*/
protected function obtainLock(Repository $cache, string $key): bool
{
return $key === $cache->remember('vapor:schedule:lock', 60, function () use ($key) {
return $key;
});
}

/**
* Release the lock for the schedule.
*/
protected function releaseLock(Repository $cache): void
{
$cache->forget('vapor:schedule:lock');
}
}
7 changes: 6 additions & 1 deletion src/VaporServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Laravel\Vapor\Console\Commands\OctaneStatusCommand;
use Laravel\Vapor\Console\Commands\VaporHealthCheckCommand;
use Laravel\Vapor\Console\Commands\VaporQueueListFailedCommand;
use Laravel\Vapor\Console\Commands\VaporScheduleCommand;
use Laravel\Vapor\Console\Commands\VaporWorkCommand;
use Laravel\Vapor\Http\Controllers\SignedStorageUrlController;
use Laravel\Vapor\Http\Middleware\ServeStaticAssets;
Expand Down Expand Up @@ -173,7 +174,11 @@ protected function registerCommands()
return new VaporHealthCheckCommand;
});

$this->commands(['command.vapor.work', 'command.vapor.queue-failed', 'command.vapor.health-check']);
$this->app->singleton('command.vapor.schedule', function () {
return new VaporScheduleCommand;
});

$this->commands(['command.vapor.work', 'command.vapor.queue-failed', 'command.vapor.health-check', 'command.vapor.schedule']);
}

/**
Expand Down
75 changes: 75 additions & 0 deletions tests/Unit/VaporScheduleCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Laravel\Vapor\Tests\Unit;

use Carbon\Carbon;
use Illuminate\Cache\Repository;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Laravel\Vapor\VaporServiceProvider;
use Mockery;
use Orchestra\Testbench\TestCase;

class VaporScheduleCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Carbon::setTestNow('2021-01-01 00:00:00');

Str::createUuidsUsing(function () {
return 'test-schedule-lock-key';
});
}

protected function getPackageProviders($app): array
{
return [
VaporServiceProvider::class,
];
}

public function test_scheduler_is_invoked_when_invalid_cache_is_configured()
{
$fake = Mockery::mock(Repository::class);
Cache::shouldReceive('getDefaultDriver')->once()->andReturn('array');
$fake->shouldNotReceive('remember');
if (version_compare($this->app->version(), 10, '>=')) {
$fake->shouldReceive('forget')->once()->with('illuminate:schedule:interrupt')->andReturn(true);
}
if (! Str::startsWith($this->app->version(), '9')) {
Cache::shouldReceive('driver')->once()->andReturn($fake);
}
$fake->shouldNotReceive('forget')->with('vapor:schedule:lock');

$this->artisan('vapor:schedule')
->assertExitCode(0);
}

public function test_scheduler_is_called_at_the_top_of_the_minute()
{
Cache::shouldReceive('getDefaultDriver')->once()->andReturn('dynamodb');
Cache::shouldReceive('driver')->andReturn($fake = Mockery::mock(Repository::class));
$fake->shouldReceive('remember')->once()->with('vapor:schedule:lock', 60, Mockery::any())->andReturn('test-schedule-lock-key');
if (version_compare($this->app->version(), 10, '>=')) {
$fake->shouldReceive('forget')->once()->with('illuminate:schedule:interrupt')->andReturn(true);
}
$fake->shouldReceive('forget')->once()->with('vapor:schedule:lock')->andReturn(true);

$this->artisan('vapor:schedule')
->assertExitCode(0);
}

public function test_scheduler_is_not_invoked_if_lock_cannot_be_obtained()
{
Cache::shouldReceive('getDefaultDriver')->once()->andReturn('dynamodb');
Cache::shouldReceive('driver')->andReturn($fake = Mockery::mock(Repository::class));
$fake->shouldReceive('remember')->once()->with('vapor:schedule:lock', 60, Mockery::any())->andReturn('test-locked-schedule-lock-key');
$fake->shouldNotReceive('forget')->with('illuminate:schedule:interrupt')->andReturn(true);
$fake->shouldNotReceive('forget')->with('vapor:schedule:lock');

$this->artisan('vapor:schedule')
->assertExitCode(1);
}
}

0 comments on commit 03bc115

Please sign in to comment.