Skip to content

Instantly share code, notes, and snippets.

@davutkmbr
Last active March 17, 2025 17:21
Show Gist options
  • Save davutkmbr/43d442a39e2735274687cecdb70c2e74 to your computer and use it in GitHub Desktop.
Save davutkmbr/43d442a39e2735274687cecdb70c2e74 to your computer and use it in GitHub Desktop.
Laravel Debounced Jobs
<?php
namespace App\Jobs\Debounce;
use Illuminate\Support\Facades\Redis;
use App\Jobs\Debounce\EnsureToRunLastJob;
use Illuminate\Support\Str;
class DebounceService
{
public static function dispatch(mixed $job, int $ttl, string $key = null): void
{
$key ??= self::getKey($job);
$uniqueId = self::getUniqueId();
// Set the key with the unique ID and a TTL of 10 times the job's TTL because the job will be delayed.
Redis::set($key, $uniqueId, 'EX', $ttl * 10);
dispatch($job->delay($ttl)->through(new EnsureToRunLastJob($key, $uniqueId)));
}
protected static function getKey(mixed $job): string
{
$key = match (true) {
method_exists($job, 'debounceKey') => $job->debounceKey(),
method_exists($job, 'uniqueId') => $job->uniqueId(),
default => get_class($job),
};
return sprintf('debounced_job:%s', $key);
}
protected static function getUniqueId(): string
{
return Str::orderedUuid();
}
}
<?php
namespace App\Jobs\Debounce;
use Illuminate\Support\Facades\Redis;
class EnsureToRunLastJob
{
public function __construct(
protected string $key,
protected string $uniqueId
) {
//
}
public function handle($job, $next)
{
$storedUniqueId = Redis::get($this->key);
if ($storedUniqueId === $this->uniqueId) {
Redis::del($this->key);
$next($job);
return;
}
$job->delete();
}
}
<?php
use App\Jobs\Debounce\DebounceService;
/**
* Global debounce helper function.
*/
function debounce(mixed $job, int $ttl, ?string $key = null): void
{
app(DebounceService::class)->dispatch($job, $ttl, $key);
}
/**
* Debounce a job if a condition is met.
*/
function debounce_if(bool $boolean, mixed $job, int $ttl, ?string $key = null): void
{
if ($boolean) {
debounce($job, $ttl, $key);
} else {
dispatch($job);
}
}
@Artem-Schander
Copy link

Prevent a potential race condition

<?php

namespace App\Services;

use Illuminate\Support\Facades\Redis;

class EnsureToRunLastJob
{
    protected string $key;
    protected string $uniqueId;

    public function __construct(string $key, string $uniqueId)
    {
        $this->key = $key;
        $this->uniqueId = $uniqueId;
    }

    public function handle($job, $next)
    {
        // Lua script for atomic check and delete
        $lua = <<<LUA
            if redis.call('get', KEYS[1]) == ARGV[1] then
                redis.call('del', KEYS[1])
                return 1
            else
                return 0
            end
        LUA;

        // Execute the Lua script atomically
        $result = Redis::eval($lua, 1, $this->key, $this->uniqueId);

        if ($result == 1) {
            // This job is the last one -> process it
            $next($job);
            return;
        }

        // Otherwise, delete the job since a newer one is scheduled
        $job->delete();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment