Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Add job middleware to prevent overlapping jobs #34794

Merged
merged 9 commits into from Oct 14, 2020
79 changes: 79 additions & 0 deletions src/Illuminate/Queue/Middleware/PreventOverlappingJobs.php
@@ -0,0 +1,79 @@
<?php

namespace Illuminate\Queue\Middleware;

use Illuminate\Contracts\Cache\Repository as Cache;

class PreventOverlappingJobs
{
/**
* The key of the job.
*
* @var string
*/
public $key;
paras-malhotra marked this conversation as resolved.
Show resolved Hide resolved

/**
* The prefix of the lock key.
*
* @var string
*/
public $prefix;

/**
* Create a new overlapping jobs middleware instance.
*
* @param string $key
* @param int $expiresAt
* @param string $prefix
*
* @return void
*/
public function __construct($key = '', $prefix = 'overlap:')
{
$this->key = $key;
$this->prefix = $prefix;
}

/**
* Process the job.
*
* @param mixed $job
* @param callable $next
* @return mixed
*/
public function handle($job, $next)
{
$lock = app(Cache::class)->lock($this->getLockKey($job));

if ($lock->get()) {
$next($job);
paras-malhotra marked this conversation as resolved.
Show resolved Hide resolved

$lock->release();
}
}

/**
* Set the prefix of the lock key.
*
* @param string $prefix
* @return $this
*/
public function withPrefix($prefix)
{
$this->prefix = $prefix;

return $this;
}

/**
* Get the lock key.
*
* @param mixed $job
paras-malhotra marked this conversation as resolved.
Show resolved Hide resolved
* @return string
*/
public function getLockKey($job)
{
return $this->prefix.get_class($job).':'.$this->key;
}
}
87 changes: 87 additions & 0 deletions tests/Integration/Queue/PreventOverlappingJobsTest.php
@@ -0,0 +1,87 @@
<?php

namespace Illuminate\Tests\Integration\Queue;

use Illuminate\Bus\Dispatcher;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Queue\CallQueuedHandler;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\PreventOverlappingJobs;
use Mockery as m;
use Orchestra\Testbench\TestCase;

/**
* @group integration
*/
class PreventOverlappingJobsTest extends TestCase
{
protected function tearDown(): void
{
parent::tearDown();

m::close();
}

public function testNonOverlappingJobsAreExecuted()
{
OverlappingTestJob::$handled = false;
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app);

$job = m::mock(Job::class);

$job->shouldReceive('hasFailed')->andReturn(false);
$job->shouldReceive('isReleased')->andReturn(false);
$job->shouldReceive('isDeletedOrReleased')->andReturn(false);
$job->shouldReceive('delete')->once();

$instance->call($job, [
'command' => serialize($command = new OverlappingTestJob),
]);

$lockKey = (new PreventOverlappingJobs)->getLockKey($command);

$this->assertTrue(OverlappingTestJob::$handled);
$this->assertTrue($this->app->get(Cache::class)->lock($lockKey, 10)->acquire());
}

public function testOverlappingJobsAreNotExecuted()
{
OverlappingTestJob::$handled = false;
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app);

$lockKey = (new PreventOverlappingJobs)->getLockKey($command = new OverlappingTestJob);
$this->app->get(Cache::class)->lock($lockKey, 10)->acquire();

$job = m::mock(Job::class);

$job->shouldReceive('hasFailed')->andReturn(false);
$job->shouldReceive('isReleased')->andReturn(false);
$job->shouldReceive('isDeletedOrReleased')->andReturn(false);
$job->shouldReceive('delete')->once();

$instance->call($job, [
'command' => serialize($command),
]);

$this->assertFalse(OverlappingTestJob::$handled);
}
}

class OverlappingTestJob
{
use InteractsWithQueue, Queueable;

public static $handled = false;

public function handle()
{
static::$handled = true;
}

public function middleware()
{
return [new PreventOverlappingJobs];
}
}