Skip to content

Commit

Permalink
Merge branch '2.0' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
nunomaduro committed Jan 16, 2024
2 parents e6d5b33 + 24e5a08 commit ed4f99b
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 19 deletions.
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
"require": {
"php": "^7.2|^8.0",
"aws/aws-sdk-php": "^3.80",
"psr/http-message": "^1.0",
"guzzlehttp/promises": "^1.4.0",
"guzzlehttp/promises": "^1.4|^2.0",
"guzzlehttp/guzzle": "^6.3|^7.0",
"hollodotme/fast-cgi-client": "^3.0",
"illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
Expand Down
11 changes: 8 additions & 3 deletions src/ConfiguresRedis.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ protected function ensureRedisIsConfigured()

Config::set('database.redis', array_merge(Arr::except(Config::get('database.redis', []), ['default', 'cache']), [
'client' => $_ENV['REDIS_CLIENT'] ?? 'phpredis',
'options' => array_merge(Config::get('database.redis.options', []), [
'cluster' => $_ENV['REDIS_CLUSTER'] ?? 'redis',
]),
'options' => array_merge(
Config::get('database.redis.options', []),
array_filter([
'cluster' => $_ENV['REDIS_CLUSTER'] ?? 'redis',
'scheme' => $_ENV['REDIS_SCHEME'] ?? null,
'context' => array_filter(['cafile' => $_ENV['REDIS_SSL_CA'] ?? null]),
])
),
'clusters' => array_merge(Config::get('database.redis.clusters', []), [
'default' => [
[
Expand Down
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');
}
}
23 changes: 10 additions & 13 deletions src/Runtime/Handlers/WarmerHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Laravel\Vapor\Runtime\Handlers;

use Aws\Lambda\LambdaClient;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\Utils;
use Laravel\Vapor\Contracts\LambdaEventHandler;
use Laravel\Vapor\Runtime\ArrayLambdaResponse;
use Laravel\Vapor\Runtime\Logger;
Expand All @@ -14,15 +14,14 @@ class WarmerHandler implements LambdaEventHandler
/**
* Handle an incoming Lambda event.
*
* @param array $event
* @return \Laravel\Vapor\Contracts\LambdaResponse
*/
public function handle(array $event)
{
try {
Logger::info('Executing warming requests...');

Promise\settle(
Utils::settle(
$this->buildPromises($this->lambdaClient(), $event)
)->wait();
} catch (Throwable $e) {
Expand All @@ -37,21 +36,19 @@ public function handle(array $event)
/**
* Build the array of warmer invocation promises.
*
* @param \Aws\Lambda\LambdaClient $lambda
* @param array $event
* @return array
*/
protected function buildPromises(LambdaClient $lambda, array $event)
{
return collect(range(1, $event['concurrency'] - 1))
->mapWithKeys(function ($i) use ($lambda, $event) {
return ['warmer-'.$i => $lambda->invokeAsync([
'FunctionName' => $event['functionName'],
'Qualifier' => $event['functionAlias'],
'LogType' => 'None',
'Payload' => json_encode(['vaporWarmerPing' => true]),
])];
})->all();
->mapWithKeys(function ($i) use ($lambda, $event) {
return ['warmer-'.$i => $lambda->invokeAsync([
'FunctionName' => $event['functionName'],
'Qualifier' => $event['functionAlias'],
'LogType' => 'None',
'Payload' => json_encode(['vaporWarmerPing' => true]),
])];
})->all();
}

/**
Expand Down
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 ed4f99b

Please sign in to comment.