Back to projects
Feb 18, 2023
5 min read

Laravel debounce

a laravel package that gives you a debounce effect on jobs, notifications and artisan commands with a nice report of occurrences.

logo

Laravel debounce

by zackaj

Laravel-debounce allows you to accumulate / debounce a job,notification or command to avoid spamming your users and your app’s queue.

It also tracks and registers every request occurrence and gives you a nice report tracking with information like ip address and authenticated user per request.

Table of Contents

Introduction

This laravel package uses UniqueJobs (atomic locks) and caching to run only one instance of a task in a debounced interval of x seconds delay.

Everytime a new activity is recoreded (occurrence), the execution is delayed with x seconds.

Features

Demo

A debounced notification to bulk notify users about new uploaded files.

See Code

FileUploaded.php

<?php

namespace App\Notifications;

use App\Models\File;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

class FileUploaded extends Notification
{
    use Queueable;

    public function __construct(public File $file) {}

    public function via(object $notifiable): array
    {
        return ['database'];
    }

    public function toArray(object $notifiable): array
    {
        return [
            'files' => $this->file->user->files()
                ->where('created_at', '>=', $this->file->created_at)
                ->get(),
        ];
    }
}

DemoController.php

<?php

namespace App\Http\Controllers;

use App\Models\File;
use App\Models\User;
use App\Notifications\FileUploaded;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Zackaj\LaravelDebounce\Facades\Debounce;

class DemoController extends Controller
{
    public function normalNotification(Request $request)
    {
        $user = $request->user();
        $file = File::factory()->create(['user_id' => $user->id]);
        $otherUsers = User::query()->whereNot('id', $user->id)->get();

        Notification::send($otherUsers, new FileUploaded($file));

        return back();
    }

    public function debounceNotification(Request $request)
    {
        $user = $request->user();
        $file = File::factory()->create(['user_id' => $user->id]);
        $otherUsers = User::query()->whereNot('id', $user->id)->get();

        Debounce::notification(
            notifiables: $otherUsers,
            notification:new FileUploaded($file),
            delay: 5,
            uniqueKey:$user->id,
        );

        return back();
    }
}

Installation

Prerequisites

  • Laravel application (11.x , 10.x should be fine)
  • Up and running cache system that supports atomic locks
  • Up and running queue worker

Composer

  composer require zackaj/laravel-debounce

Usage

Basic usage

You can debounce existing jobs, notifications and commands with zero setup.

Warning you can’t access report or track the requests without extending the package’s classes, see Advanced usage

use Zackaj\LaravelDebounce\Facades\Debounce;


//job
Debounce::job(
    job:new Job(),//replace
    delay:5,//delay in seconds
    uniqueKey:auth()->user()->id,//debounce per Job class name + uniqueKey
    sync:false, //optional, job will be fired to the queue
);

//notification
Debounce::notification(
    notifiables: auth()->user(),
    notification: new Notification(),//replace
    delay: 5,
    uniqueKey: auth()->user()->id,
    sendNow: false,
);

//command
Debounce::command(
    command: new Command(),//replace
    delay: 5,
    uniqueKey: $request->ip(),
    parameters: ['name' => 'zackaj'],//see Artisan::call() signature
    toQueue: false,//optional, send command to the queue when executed
    outputBuffer: null,//optional, //see Artisan::call() signature
);

Advanced usage

In order to use:

your existing jobs, notificaitons and commands must extend:

use Zackaj\LaravelDebounce\DebounceJob;
use Zackaj\LaravelDebounce\DebounceNotification;
use Zackaj\LaravelDebounce\DebounceCommand;

or just generate new ones using the available make commands.

Make commands

  • Notification
php artisan make:debounce-notification TestNotification
  • Job
php artisan make:debounce-job TestJob
  • Command
php artisan make:debounce-command TestCommand

No facade usage

Alternatively, now you can debounce from the job, notification and command instances directly without using the Debounce facade used in Basic usage

(new Job())->debounce(...);

(new Notification())->debounce(...);

(new Command())->debounce(...);

Report Tracking

Laravel-debounce uses the cache to store every request occurrence, use getReport() method within your debounceables to access the report chain that has a collection of occurrences.

Every report will have one occurrence minimum.

<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Zackaj\LaravelDebounce\DebounceJob;

class Jobless extends DebounceJob implements ShouldQueue
{
    use Dispatchable;

    public function handle(): void
    {
        $this->getReport()->occurrences;//collection of occurrences
        $this->getReport()->occurrences->count();
        $this->getReport()->occurrences->first()->happenedAt;
        $this->getReport()->occurrences->first()->ip;
        $this->getReport()->occurrences->first()->ips;
        $this->getReport()->occurrences->first()->requestHeaders;//HeaderBag
        $this->getReport()->occurrences->first()->user;//authenticated user | null
    }
}

Before After Hooks

If you wish to run some code before and/or after firing the debounceables you can use the available hooks.

Important: after() hook could run before your debounceable is handled if it’s sent to the queue when:

  • sendNow==false and your notification implements ShouldQueue
  • sync==false and your job implements ShouldQueue
  • toQueue==true (command)

see: Basic usage

Debounce job

<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
    public function before(): void
    {
        //run before dispatching the job
    }

    public function after(): void
    {
        //run after dispatching the job
    }
}

Debounce notification

You get the $notifiables injected into the hooks.

<?php
...

class FileUploaded extends DebounceNotification
{
...
    public function before($notifiables): void
    {
        //run before sending the notification
    }

    public function after($notifiables): void
    {
        //run after sending the notification
    }
}

Debounce command

Due to limitations, the hook methods must be static.

<?php
...

class Test extends DebounceCommand
{
...
    public static function before(): void
    {
        //run before executing the command
    }

    public static function after(): void
    {
        //run after executing the command
    }

}

Override Timestamp

By default laravel-debounce debounces from the last occurrence happenedAt timestamp

public function getLastActivityTimestamp(): ?Carbon
{
    return $this->getReport()->occurrences->last()->happenedAt;
}

You can override this method in your debounceables in order to debounce from a custom timestamp of choice, if null returned the debouncer will fallback to the default implementation above.

Debounce job

<?php
...
class Jobless extends DebounceJob implements ShouldQueue
{
...
    public function getLastActivityTimestamp(): ?Carbon
    {
        return Message::latest()->first()?->seen_at;
    }
}

Debounce notification

You get the $notifiables injected into the method.

<?php
...

class FileUploaded extends DebounceNotification
{
...
    public function getLastActivityTimestamp(mixed $notifiables): ?Carbon
    {
        return $this->file->user->files->latest()->first()?->created_at;
    }
}

Debounce command

Due to limitations, the method must be static.

<?php
...

class Test extends DebounceCommand
{
...
    public static function getLastActivityTimestamp(): ?Carbon
    {
        return User::latest()->first()?->created_at;
    }
}

Bonus CLI Debounce

For fun, you can actually debounce commands from the CLI using debounce:command Artisan command.

php artisan debounce:command 5 uniqueKey app:test

here’s the signature for the command: php artisan debounce:command {delay} {uniqueKey} {signature*}

Debugging And Monitoring

I recommend using Laravel telescope to see the debouncer live in queues tab and to debug any failures.

Known Issues

1- Unique lock gets stuck sometimes when jobs fail github issue

  • cause: this happens when deleted models are unserialized causing the job to fail without clearing the lock.

  • solution: If you’re using database cache driver delete the entry from job_locks table.

Contributing

Contributions, issues and suggestions are always welcome! See contributing.md for ways to get started.

License

MIT