Skip to content

Commit 9a81c08

Browse files
committed
Update docs
1 parent d2dc6fa commit 9a81c08

File tree

5 files changed

+115
-87
lines changed

5 files changed

+115
-87
lines changed

README.md

Lines changed: 103 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,135 @@
11
# Laravel Queue Debouncer
2-
### Really easy debouncing of Laravel queue jobs via a wrapper job
2+
### Easy queue job debouncing
33

44
## Requirements
5-
* PHP >= 7.0
6-
* Laravel >= 5.5
7-
* A correctly configured asynchronous queue driver (tested with Redis and database drivers, but anything should work fine)
8-
* A correctly configured cache driver
5+
* Laravel >= 6.0
6+
* An async queue driver
7+
8+
**Note:** v2.0 requires at least Laravel 6. For Laravel 5.5 ..< 6.0 support, check out [v1.0.2](https://github.com/mpbarlow/laravel-queue-debouncer/tree/1.0.2)
99

1010
## Installation
1111
`composer require mpbarlow/laravel-queue-debouncer`
1212

13+
## Background
14+
This package allows any queue job in your Laravel application to be debounced, meaning that no matter how many times it’s dispatched within the specified timeout window, it will only actually be ran once.
15+
16+
For example, imagine you want to send an email each time a user changes their contact details, but you don’t want to spam them if they make several changes in a short space of time. Debouncing the job with a five minute wait would ensure they only receive a single email, no matter how many changes they make within that five minutes.
17+
18+
Version 2.0 is a complete rewrite and brings numerous improvements to the capabilities and ergonomics of the package, as well as thorough test coverage.
19+
1320
## Usage
14-
The package comes in two main parts: a wrapper job `Debounce` that is dispatched immediately, and an abstract superclass
15-
`DebouncedJob` that your debounced jobs should extend.
16-
17-
`Debounce`’s constructor takes at least two arguments; `string $debounceable`, which is the fully-qualified class name of
18-
the job you wish to debounce, and `int $waitTime`, which is the time in seconds that will be waited after the last dispatch of the job before it is fired. Any additional arguments will be passed directly to the constructor of the debounced job.
19-
`Debounce` is also responsible for generating the unique ID that will be assigned to your job; out of the box it uses
20-
[ramsey/uuid](https://github.com/ramsey/uuid), which ships with Laravel, to generate a random UUID4 string. However, if
21-
this does not suit your needs, you can subclass `Debounced` and override `generateUniqueId()`.
22-
23-
If you would prefer to implement your own wrapper class, the interface `DebouncesJobs` contains the methods your
24-
customer wrapper must implement and call from its `handle()` method. You are responsible for caching the unique ID in a
25-
way that the debounced job can retreive, however the `ProvidesCacheKey` trait contains the default implementation used by
26-
both `Debounce` and `DebouncedJob`.
27-
28-
`DebouncedJob` is the class that your debounced jobs should extend. It receives the unique ID from the
29-
wrapper job via the constructor (if you need to override the default constructor be sure to call `parent::__construct($uniqueId)`
30-
`$uniqueId` is always the first argument your constructor receives, followed by any additional parameters you may have
31-
passed). By default it takes responsibility for your job’s `handle()` method, checking whether the job should run, and
32-
if so, calling the `debounced()` method. This is the method you should implement with your job logic. If the job should
33-
not run, it is automatically deleted and `debounced()` is never called.
34-
35-
If you can’t subclass `DebouncedJob` for your jobs, this package also provides a trait `InteractsWithDebouncer`,
36-
which provides the default implementation for `handle()`, and a method `shouldRun()` which returns a boolean value
37-
indicating whether the job should run.
38-
39-
If you prefer or need to implement your logic in the normal `handle()` method (for example if you need access to objects
40-
via dependency injection), override the provided implementation and call `shouldRun()` manually to determine your course
41-
of action.
42-
43-
## Example
44-
SimpleDebouncedJob.php
21+
Debounced jobs can largely be treated like any other dispatched job. The debouncer takes two arguments, the actual `$job` you want to run, and the `$wait` period.
22+
23+
As with a regular dispatch, `$job` can be either a class implementing `Illuminate\Foundation\Bus\Dispatchable` or a closure. `$wait` accepts any argument that the `delay` method on a dispatch accepts (i.e. a `DateTimeInterface` or a number of seconds).
24+
25+
The debouncer returns an instance of `Illuminate\Foundation\Bus\PendingDispatch`, meaning the debouncing process itself may be assigned to a different queue or otherwise manipulated.
26+
27+
### Calling the debouncer
28+
There are several ways to initiate debouncing of a job:
29+
30+
#### Dependency Injection
4531
```php
46-
...
32+
use App\Jobs\MyJob;
33+
use Mpbarlow\LaravelQueueDebouncer\Debouncer;
4734

48-
public function debounced()
35+
class MyController
4936
{
50-
echo "Hello, I am debounced. I have no data though :(\n";
37+
public function doTheThing(Debouncer $debouncer)
38+
{
39+
$debouncer->debounce(new MyJob(), 30);
40+
41+
// The class is also invokable:
42+
$debouncer(new MyJob(), 30);
43+
}
5144
}
5245
```
5346

54-
DebouncedJobWithExtraArguments.php
47+
#### Facade
5548
```php
56-
...
49+
use App\Jobs\MyJob;
50+
use Mpbarlow\LaravelQueueDebouncer\Facade\Debouncer;
5751

58-
protected $some;
59-
protected $extra;
60-
protected $arguments;
52+
Debouncer::debounce(new MyJob, 30);
53+
```
6154

62-
public function __construct($uniqueId, $some, $extra, $arguments)
63-
{
64-
// We must make sure to provide the superclass constructor with the unique ID
65-
parent::__construct($uniqueId);
55+
#### Helper function
56+
```php
57+
use App\Jobs\MyJob;
6658

67-
$this->some = $some;
68-
$this->extra = $extra;
69-
$this->arguments = $arguments;
70-
}
59+
debounce(new MyJob(), 30);
60+
```
61+
62+
Of course, you may substitute the job class for a closure.
63+
64+
When monitoring the queue, you will see an entry for the package’s internal dispatcher each time the debounced job is queued, but the job itself will only run once, when the final wait time has expired.
65+
66+
For example, assuming the following code was ran at exactly 9am:
7167

72-
public function debounced()
68+
```php
69+
class MyJob
70+
{
71+
use Dispatchable;
72+
73+
public function handle()
7374
{
74-
echo "Hello, I am debounced. I have {$this->some} {$this->extra} {$this->arguments} though! :D\n";
75+
echo Hello!\n”;
7576
}
77+
}
78+
79+
$job = new MyJob();
7680

81+
debounce($job, now()->addSeconds(5));
82+
sleep(3);
83+
84+
debounce($job, now()->addSeconds(5));
85+
sleep(3);
86+
87+
debounce($job, now()->addSeconds(5));
7788
```
7889

79-
```php
80-
dispatch(new Debounce(SimpleDebouncedJob::class, 10));
81-
sleep(1);
82-
dispatch(new Debounce(SimpleDebouncedJob::class, 10));
83-
sleep(1);
84-
dispatch(new Debounce(SimpleDebouncedJob::class, 10));
85-
sleep(1);
86-
87-
dispatch(new Debounce(DebouncedJobWithExtraArguments::class, 10, 'some', 'extra', 'arguments'));
88-
sleep(1);
89-
dispatch(new Debounce(DebouncedJobWithExtraArguments::class, 10, 'some', 'extra', 'arguments'));
90-
sleep(1);
91-
dispatch(new Debounce(DebouncedJobWithExtraArguments::class, 10, 'some', 'extra', 'arguments'));
92-
sleep(1);
90+
you should expect the following activity on your queue:
91+
9392
```
93+
[2020-03-11 09:00:05][vHmqrBYeLtK3Lbiq5TsTZxBo2igaCZHC] Processing: Closure (DispatcherFactory.php:28)
94+
[2020-03-11 09:00:05][vHmqrBYeLtK3Lbiq5TsTZxBo2igaCZHC] Processed: Closure (DispatcherFactory.php:28)
95+
[2020-03-11 09:00:08][LXdzLvilh5qhew7akNDnibCjaXksG81X] Processing: Closure (DispatcherFactory.php:28)
96+
[2020-03-11 09:00:08][LXdzLvilh5qhew7akNDnibCjaXksG81X] Processed: Closure (DispatcherFactory.php:28)
97+
[2020-03-11 09:00:11][MnPIqk5fCwXjiVzuwPjkkOdPPBn0xR4d] Processing: Closure (DispatcherFactory.php:28)
98+
[2020-03-11 09:00:11][MnPIqk5fCwXjiVzuwPjkkOdPPBn0xR4d] Processed: Closure (DispatcherFactory.php:28)
99+
[2020-03-11 09:00:11][I2hvBoCB71qZQeD4umn5dd90zJUCAlJ5] Processing: App\Jobs\MyJob
100+
Hello!
101+
[2020-03-11 09:00:11][I2hvBoCB71qZQeD4umn5dd90zJUCAlJ5] Processed: App\Jobs\MyJob
102+
```
103+
104+
## Customising Behaviour
105+
This package provides a few knobs and dials to customise things to your needs. To override the default behaviour, you should publish the config file:
106+
107+
```
108+
php artisan vendor:publish --provider="Mpbarlow\LaravelQueueDebouncer\ServiceProvider"
109+
```
110+
111+
This will copy `queue_debouncer.php` to your config directory.
112+
113+
### Cache key provider
114+
To support multiple jobs being debounced, the package generates a unique key in the cache for each job type. This key is used as a lookup when the wait for a given dispatch expires, to determine whether the job should be ran or not.
115+
116+
The default implementation works as follows:
117+
* For class-based jobs, use the fully-qualified class name
118+
* For closure-based jobs, compute the SHA1 hash of a reflected representation of the closure
119+
* Prepend the `cache_prefix` value from the config, separated by a colon (‘:’)
120+
121+
If the default behaviour works for you, but you’d like to use a different cache prefix, you may simply override that value.
94122

95-
`SimpleDebouncedJob` and `DebouncedJobWithExtraArguments` will each run approximately 10 seconds after their last dispatch,
96-
respectively, producing the following output:
123+
Alternatively, you can provide your own class to produce a key for a given job. This is useful in cases where you have jobs that are implemented as different classes but should be considered the same job for the purposes of debouncing.
97124

98-
![](README/debouncer.jpg)
125+
Your provider class must implement `Mpbarlow\LaravelQueueDebouncer\Contracts\CacheKeyProvider`. Please note that your class is responsible for fetching and prepending the cache prefix should you still desire this behaviour—it will not be added automatically.
99126

100-
It's worth noting that if your job does not produce any output it may appear that it is running every single time. In a
101-
sense it is, but only to determine whether or not execution should continue. The actual `debounced()` method containing
102-
your logic is only executed when the bouncing has settled.
127+
**Note:** Because of the limited ways we have to produce a value representation of a closure, only the _exact same closure_ will produce the same cache key. So, while closures originating from the same line in the same file will produce the same key on subsequent requests, two closures with the same content dispatched from different places will not.
103128

104-
See also the included Examples folder.
129+
### Unique identifier provider
130+
Each time a debounced job is dispatched, a unique identifier is stored against the cache key for the job. When the wait time expires, if that identifier matches the value inserted, the debouncer knows that no more recent instances of the job have been dispatched, and therefore it is safe to dispatch it.
105131

106-
## Potential Issues
107-
Race conditions with certain drivers, particularly when multiple processes work the queue, or when wait times are very
108-
short, can mean that the job that executes is not always the last one that was dispatched. It will still only execute
109-
once, but it is important to bear this in mind if your job is strictly dependent on dispatch order.
132+
The default implementation produces a UUID v4 for each dispatch. If you need to override this you may do so via the config. As with the cache key provider, your class must implement `Mpbarlow\LaravelQueueDebouncer\Contracts\UniqueIdentifierProvider`.
110133

111-
## License
134+
## Licence
112135
This package is open-source software provided under the [The MIT License](https://opensource.org/licenses/MIT).

docs/debouncer.jpg

-417 KB
Binary file not shown.

src/Facade/Debouncer.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,11 @@
44
namespace Mpbarlow\LaravelQueueDebouncer\Facade;
55

66

7-
use Closure;
8-
use DateInterval;
9-
use DateTimeInterface;
10-
use Illuminate\Foundation\Bus\Dispatchable;
11-
use Illuminate\Foundation\Bus\PendingDispatch;
127
use Illuminate\Support\Facades\Facade;
138

149
/**
1510
* @see \Mpbarlow\LaravelQueueDebouncer\Debouncer
16-
* @method static PendingDispatch debounce(Dispatchable|Closure $job, DateTimeInterface|DateInterval|int|null $wait)
11+
* @method static \Illuminate\Foundation\Bus\PendingDispatch debounce(\Illuminate\Foundation\Bus\Dispatchable|\Closure $job, \DateTimeInterface|\DateInterval|int|null $wait)
1712
*/
1813
class Debouncer extends Facade
1914
{

src/Support/helpers.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
<?php
22

3+
use Illuminate\Foundation\Bus\Dispatchable;
4+
use Illuminate\Foundation\Bus\PendingDispatch;
35
use Mpbarlow\LaravelQueueDebouncer\Debouncer;
46

57
if (! function_exists('debounce')) {
8+
/**
9+
* @param Dispatchable|Closure $job
10+
* @param DateTimeInterface|DateInterval|int|null $wait
11+
* @return PendingDispatch
12+
*/
613
function debounce($job, $wait)
714
{
815
return app(Debouncer::class)($job, $wait);

tests/CacheKeyProviderTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace Mpbarlow\LaravelQueueDebouncer\Tests;
55

66

7+
use Illuminate\Foundation\Bus\Dispatchable;
78
use Mpbarlow\LaravelQueueDebouncer\Support\CacheKeyProvider;
89
use ReflectionFunction;
910

@@ -74,4 +75,6 @@ public function it_generates_a_consistent_key_for_closures()
7475
}
7576
}
7677

77-
class DummyJob {}
78+
class DummyJob {
79+
use Dispatchable;
80+
}

0 commit comments

Comments
 (0)