From 03bc11599dd880d4e0e13caf000e265505363f4c Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Tue, 19 Dec 2023 08:34:28 +0000 Subject: [PATCH] [2.x] Adds scheduler proxy command to normalize sub-minute schedules (#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 Co-authored-by: Taylor Otwell --- src/Console/Commands/VaporScheduleCommand.php | 98 +++++++++++++++++++ src/VaporServiceProvider.php | 7 +- tests/Unit/VaporScheduleCommandTest.php | 75 ++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/Console/Commands/VaporScheduleCommand.php create mode 100644 tests/Unit/VaporScheduleCommandTest.php diff --git a/src/Console/Commands/VaporScheduleCommand.php b/src/Console/Commands/VaporScheduleCommand.php new file mode 100644 index 0000000..83b555a --- /dev/null +++ b/src/Console/Commands/VaporScheduleCommand.php @@ -0,0 +1,98 @@ +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'); + } +} diff --git a/src/VaporServiceProvider.php b/src/VaporServiceProvider.php index 0f7c0dc..7533718 100644 --- a/src/VaporServiceProvider.php +++ b/src/VaporServiceProvider.php @@ -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; @@ -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']); } /** diff --git a/tests/Unit/VaporScheduleCommandTest.php b/tests/Unit/VaporScheduleCommandTest.php new file mode 100644 index 0000000..7008f43 --- /dev/null +++ b/tests/Unit/VaporScheduleCommandTest.php @@ -0,0 +1,75 @@ +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); + } +}