-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ThrottlesExceptionsWithRedis job middleware
- Loading branch information
1 parent
92a1ce8
commit 35071ab
Showing
3 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
62 changes: 62 additions & 0 deletions
62
src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
<?php | ||
|
||
namespace Illuminate\Queue\Middleware; | ||
|
||
use Illuminate\Container\Container; | ||
use Illuminate\Contracts\Redis\Factory as Redis; | ||
use Illuminate\Redis\Limiters\DurationLimiter; | ||
use Illuminate\Support\InteractsWithTime; | ||
use Throwable; | ||
|
||
class ThrottlesExceptionsWithRedis extends ThrottlesExceptions | ||
{ | ||
use InteractsWithTime; | ||
|
||
/** | ||
* The Redis factory implementation. | ||
* | ||
* @var \Illuminate\Contracts\Redis\Factory | ||
*/ | ||
protected $redis; | ||
|
||
/** | ||
* The rate limiter instance. | ||
* | ||
* @var \Illuminate\Redis\Limiters\DurationLimiter | ||
*/ | ||
protected $limiter; | ||
|
||
/** | ||
* Process the job. | ||
* | ||
* @param mixed $job | ||
* @param callable $next | ||
* @return mixed | ||
*/ | ||
public function handle($job, $next) | ||
{ | ||
$this->redis = Container::getInstance()->make(Redis::class); | ||
|
||
$this->limiter = new DurationLimiter( | ||
$this->redis, $this->getKey($job), $this->maxAttempts, $this->decayMinutes * 60 | ||
); | ||
|
||
if ($this->limiter->tooManyAttempts()) { | ||
return $job->release($this->limiter->decaysAt - $this->currentTime()); | ||
} | ||
|
||
try { | ||
$next($job); | ||
|
||
$this->limiter->clear(); | ||
} catch (Throwable $throwable) { | ||
if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable)) { | ||
throw $throwable; | ||
} | ||
|
||
$this->limiter->acquire(); | ||
|
||
return $job->release($this->retryAfterMinutes * 60); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<?php | ||
|
||
namespace Illuminate\Tests\Integration\Queue; | ||
|
||
use Exception; | ||
use Illuminate\Bus\Dispatcher; | ||
use Illuminate\Bus\Queueable; | ||
use Illuminate\Contracts\Queue\Job; | ||
use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; | ||
use Illuminate\Queue\CallQueuedHandler; | ||
use Illuminate\Queue\InteractsWithQueue; | ||
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis; | ||
use Illuminate\Support\Str; | ||
use Mockery as m; | ||
use Orchestra\Testbench\TestCase; | ||
|
||
/** | ||
* @group integration | ||
*/ | ||
class ThrottlesExceptionsWithRedisTest extends TestCase | ||
{ | ||
use InteractsWithRedis; | ||
|
||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->setUpRedis(); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
parent::tearDown(); | ||
|
||
$this->tearDownRedis(); | ||
|
||
m::close(); | ||
} | ||
|
||
public function testCircuitIsOpenedForJobErrors() | ||
{ | ||
$this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); | ||
$this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); | ||
$this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); | ||
} | ||
|
||
public function testCircuitStaysClosedForSuccessfulJobs() | ||
{ | ||
$this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key = Str::random()); | ||
$this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); | ||
$this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); | ||
} | ||
|
||
public function testCircuitResetsAfterSuccess() | ||
{ | ||
$this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); | ||
$this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); | ||
$this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); | ||
$this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); | ||
$this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); | ||
} | ||
|
||
protected function assertJobWasReleasedImmediately($class, $key) | ||
{ | ||
$class::$handled = false; | ||
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); | ||
|
||
$job = m::mock(Job::class); | ||
|
||
$job->shouldReceive('hasFailed')->once()->andReturn(false); | ||
$job->shouldReceive('release')->with(0)->once(); | ||
$job->shouldReceive('isReleased')->andReturn(true); | ||
$job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); | ||
|
||
$instance->call($job, [ | ||
'command' => serialize($command = new $class($key)), | ||
]); | ||
|
||
$this->assertTrue($class::$handled); | ||
} | ||
|
||
protected function assertJobWasReleasedWithDelay($class, $key) | ||
{ | ||
$class::$handled = false; | ||
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); | ||
|
||
$job = m::mock(Job::class); | ||
|
||
$job->shouldReceive('hasFailed')->once()->andReturn(false); | ||
$job->shouldReceive('release')->withArgs(function ($delay) { | ||
return $delay >= 600; | ||
})->once(); | ||
$job->shouldReceive('isReleased')->andReturn(true); | ||
$job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); | ||
|
||
$instance->call($job, [ | ||
'command' => serialize($command = new $class($key)), | ||
]); | ||
|
||
$this->assertFalse($class::$handled); | ||
} | ||
|
||
protected function assertJobRanSuccessfully($class, $key) | ||
{ | ||
$class::$handled = false; | ||
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); | ||
|
||
$job = m::mock(Job::class); | ||
|
||
$job->shouldReceive('hasFailed')->once()->andReturn(false); | ||
$job->shouldReceive('isReleased')->andReturn(false); | ||
$job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); | ||
$job->shouldReceive('delete')->once(); | ||
|
||
$instance->call($job, [ | ||
'command' => serialize($command = new $class($key)), | ||
]); | ||
|
||
$this->assertTrue($class::$handled); | ||
} | ||
} | ||
|
||
class CircuitBreakerWithRedisTestJob | ||
{ | ||
use InteractsWithQueue, Queueable; | ||
|
||
public static $handled = false; | ||
|
||
public function __construct($key) | ||
{ | ||
$this->key = $key; | ||
} | ||
|
||
public function handle() | ||
{ | ||
static::$handled = true; | ||
|
||
throw new Exception; | ||
} | ||
|
||
public function middleware() | ||
{ | ||
return [new ThrottlesExceptionsWithRedis(2, 10, 0, $this->key)]; | ||
} | ||
} | ||
|
||
class CircuitBreakerWithRedisSuccessfulJob | ||
{ | ||
use InteractsWithQueue, Queueable; | ||
|
||
public static $handled = false; | ||
|
||
public function __construct($key) | ||
{ | ||
$this->key = $key; | ||
} | ||
|
||
public function handle() | ||
{ | ||
static::$handled = true; | ||
} | ||
|
||
public function middleware() | ||
{ | ||
return [new ThrottlesExceptionsWithRedis(2, 10, 0, $this->key)]; | ||
} | ||
} |