Skip to content

Instantly share code, notes, and snippets.

@nerdo
Forked from cabloo/DebouncedJob.php
Last active October 16, 2017 19:27
Show Gist options
  • Save nerdo/918ba3192c2c1c5f3e6424c538ee4392 to your computer and use it in GitHub Desktop.
Save nerdo/918ba3192c2c1c5f3e6424c538ee4392 to your computer and use it in GitHub Desktop.
Debounced Laravel 5.3 Jobs
<?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);
});
}
}
<?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]);
}
}
}
}
<?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