-
-
Save nerdo/918ba3192c2c1c5f3e6424c538ee4392 to your computer and use it in GitHub Desktop.
Debounced Laravel 5.3 Jobs
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
<?php | |
// Based on https://gist.github.com/cabloo/328e39a19afeaed1256f83bd4a0ba4bc | |
// The maxWait option from the previous revision was removed, but can easily be added back in as feature. | |
namespace App\Jobs; | |
use Illuminate\Queue\InteractsWithQueue; | |
use Illuminate\Contracts\Queue\ShouldQueue; | |
use Illuminate\Foundation\Bus\DispatchesJobs; | |
class DebouncedJob extends Job implements ShouldQueue | |
{ | |
use DispatchesJobs, InteractsWithQueue; | |
const DEBOUNCE_UNIXTIME_CACHE_PREFIX = 'debounce_unixtime_'; | |
const EXTRA_CACHE_MINUTES = 2; | |
/** | |
* @var array | |
*/ | |
protected $cacheKeys; | |
/** | |
* The Job that is being debounced, in serialized form. | |
* | |
* @var string | |
*/ | |
protected $serializedJob; | |
/** | |
* Amount of time (in seconds) to debounce the Job. | |
* | |
* @var int | |
*/ | |
protected $debounceSeconds; | |
/** | |
* This can be used to have separately debounced versions of the same Job. | |
* | |
* @var string | |
*/ | |
protected $debounceId; | |
/** | |
* Cache of the unserialized Job instance that was debounced. | |
* | |
* @var Job|null | |
*/ | |
protected $job; | |
public function __construct($debounceId, Job $job, $debounceSeconds) | |
{ | |
$this->serializedJob = serialize($job); | |
$this->debounceSeconds = $debounceSeconds; | |
$this->debounceId = $debounceId; | |
$this->cacheData(); | |
if ($this->debounceSeconds > 0) { | |
$this->delay($this->debounceSeconds); | |
} | |
} | |
/** | |
* Handle the Job, if it is time to. | |
*/ | |
public function handle() | |
{ | |
// Check if this or a later DebouncedJob instance will handle this task. | |
if (!$this->isReadyToHandle()) { | |
return; | |
} | |
// Prevent any future debounced Jobs from being triggered, until a new one is created. | |
$this->clearCache(); | |
// Then, dispatch the original Job that was debounced. | |
$this->dispatch(unserialize($this->serializedJob)); | |
} | |
public function getJob() | |
{ | |
return $this->job ?: $this->job = unserialize($this->serializedJob); | |
} | |
/** | |
* Determine if the requested Job should be processed immediately. | |
* | |
* @return boolean | |
*/ | |
protected function isReadyToHandle() | |
{ | |
$now = time(); | |
return (bool)collect($this->cacheKeys) | |
->first(function ($cacheKey) use ($now) { | |
$unixtime = cache()->get($cacheKey); | |
return $unixtime && $now >= $unixtime; | |
}); | |
} | |
protected function cacheData() | |
{ | |
$this->cacheKeys = [ | |
$this->getDebounceUnixtimeCacheKey() | |
]; | |
$this->cacheDebounceUnixtime(); | |
} | |
/** | |
* Store the debounce seconds in the Cache. | |
*/ | |
protected function cacheDebounceUnixtime() | |
{ | |
// Persist the value in the cache a little longer than it needs to be. | |
// Our code should automatically remove it, but if for some reason it doesn't, | |
// it'll the cache can invalidate it a few minutes after we no longer need it. | |
cache()->put( | |
$this->getDebounceUnixtimeCacheKey(), | |
time() + $this->debounceSeconds, | |
(int)ceil($this->debounceSeconds / 60) + self::EXTRA_CACHE_MINUTES | |
); | |
} | |
protected function getDebounceUnixtimeCacheKey() | |
{ | |
return self::DEBOUNCE_UNIXTIME_CACHE_PREFIX . $this->debounceId; | |
} | |
/** | |
* Clear all related Cache entries. | |
*/ | |
protected function clearCache() | |
{ | |
collect($this->cacheKeys) | |
->each(function ($cacheKey) { | |
cache()->forget($cacheKey); | |
}); | |
} | |
} |
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
<?php | |
namespace Tests\Unit\Jobs; | |
use Tests\TestCase; | |
use App\Jobs\DebouncedJob; | |
use Illuminate\Support\Facades\Queue; | |
use Illuminate\Support\Facades\Artisan; | |
use Illuminate\Foundation\Testing\DatabaseMigrations; | |
class DebouncedJobTest extends TestCase | |
{ | |
use DatabaseMigrations; | |
/** @test */ | |
public function running_a_debounced_job_with_no_delay() | |
{ | |
TestJob::reset(); | |
$debounceSeconds = 0; | |
dispatch(new DebouncedJob('TestDebounceId', new TestJob, $debounceSeconds)); | |
$this->workQueue(2); | |
$this->assertEquals(1, TestJob::$numberOfTimesRun, 'TestJob should be run immediately only once.'); | |
} | |
/** @test */ | |
public function running_a_debounced_job_with_a_delay() | |
{ | |
TestJob::reset(); | |
$debounceSeconds = 2; | |
dispatch(new DebouncedJob('TestDebounceId', new TestJob, $debounceSeconds)); | |
$this->workQueue(2); | |
$this->assertEquals(0, TestJob::$numberOfTimesRun, 'TestJob should not be run immediately.'); | |
sleep($debounceSeconds + 1); | |
$this->workQueue(2); | |
$this->assertEquals(1, TestJob::$numberOfTimesRun, 'TestJob should be run immediately only once.'); | |
} | |
/** @test */ | |
public function running_a_debounced_job_multiple_times_with_no_delay() | |
{ | |
TestJob::reset(); | |
$debounceSeconds = 0; | |
dispatch(new DebouncedJob('TestDebounceId', new TestJob, $debounceSeconds)); | |
dispatch(new DebouncedJob('TestDebounceId', new TestJob, $debounceSeconds)); | |
$this->workQueue(3); | |
$this->assertEquals(1, TestJob::$numberOfTimesRun, 'TestJob should have have been run once.'); | |
} | |
/** @test */ | |
public function running_a_debounced_job_multiple_times_with_a_delay() | |
{ | |
TestJob::reset(); | |
$debounceSeconds = 4; | |
dispatch(new DebouncedJob('TestDebounceId', new TestJob, $debounceSeconds)); | |
dispatch(new DebouncedJob('TestDebounceId', new TestJob, $debounceSeconds)); | |
$this->workQueue(3); | |
$this->assertEquals(0, TestJob::$numberOfTimesRun, 'TestJob should not be run immediately.'); | |
sleep($debounceSeconds + 1); | |
$this->workQueue(3); | |
$this->assertEquals(1, TestJob::$numberOfTimesRun, 'TestJob should have have been run once.'); | |
} | |
protected function workQueue($numItems = null) | |
{ | |
if ($numItems <= 0) { | |
while (Queue::size() > 0) { | |
Artisan::call('queue:work', ['--once' => true]); | |
} | |
} else { | |
for ($i = min(Queue::size(), $numItems); $i > 0; $i--) { | |
Artisan::call('queue:work', ['--once' => true]); | |
} | |
} | |
} | |
} |
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
<?php | |
namespace Tests\Unit\Jobs; | |
use App\Jobs\Job; | |
use Carbon\Carbon; | |
class TestJob extends Job | |
{ | |
public static $numberOfTimesRun = 0; | |
public static $lastTimeRun; | |
public function handle() { | |
static::$numberOfTimesRun++; | |
static::$lastTimeRun = Carbon::now(); | |
} | |
public static function reset() | |
{ | |
static::$numberOfTimesRun = 0; | |
static::$lastTimeRun = null; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment