Skip to content

Commit be9da01

Browse files
authored
Merge pull request #5 from mpbarlow/develop
Add support for chains and make it easier to override behaviour.
2 parents e8d0e11 + f7b3fbd commit be9da01

25 files changed

+600
-125
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
/vendor
21
composer.lock
32
/.idea
3+
.phpunit.result.cache
4+
/vendor

README.md

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
# Laravel Queue Debouncer
2+
23
### Easy queue job debouncing
34

45
## Requirements
5-
* Laravel >= 6.0
6-
* An async queue driver
6+
7+
- Laravel >= 6.0
8+
- An async queue driver
79

810
**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)
911

1012
## Installation
13+
1114
`composer require mpbarlow/laravel-queue-debouncer`
1215

1316
## 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.
1517

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.
18+
This package allows any queue job or chain 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 run once.
1719

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.
20+
For example, imagine you dispatch a job to rebuild a cache every time a record is updated, but the job is resource intensive. Debouncing the job with a five minute wait would ensure that the cache is only rebuilt once, five minutes after you finish making changes.
1921

2022
## Usage
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.
2223

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+
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.
25+
26+
As with a regular dispatch, `$job` can be either a class implementing `Illuminate\Foundation\Bus\Dispatchable`, a chain, or a closure. `$wait` accepts any argument that the `delay` method on a dispatch accepts (i.e. a `DateTimeInterface` or a number of seconds).
2427

2528
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.
2629

2730
### Calling the debouncer
28-
There are several ways to initiate debouncing of a job:
31+
32+
There are several ways to use the debouncer:
2933

3034
#### Dependency Injection
35+
3136
```php
3237
use App\Jobs\MyJob;
3338
use Mpbarlow\LaravelQueueDebouncer\Debouncer;
@@ -45,6 +50,7 @@ class MyController
4550
```
4651

4752
#### Facade
53+
4854
```php
4955
use App\Jobs\MyJob;
5056
use Mpbarlow\LaravelQueueDebouncer\Facade\Debouncer;
@@ -53,15 +59,14 @@ Debouncer::debounce(new MyJob, 30);
5359
```
5460

5561
#### Helper function
62+
5663
```php
5764
use App\Jobs\MyJob;
5865

5966
debounce(new MyJob(), 30);
6067
```
6168

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.
69+
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.
6570

6671
For example, assuming the following code was ran at exactly 9am:
6772

@@ -102,7 +107,8 @@ Hello!
102107
```
103108

104109
## 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:
110+
111+
This package provides a few hooks to customise things to your needs. To override the default behaviour, you should publish the config file:
106112

107113
```
108114
php artisan vendor:publish --provider="Mpbarlow\LaravelQueueDebouncer\ServiceProvider"
@@ -111,25 +117,54 @@ php artisan vendor:publish --provider="Mpbarlow\LaravelQueueDebouncer\ServicePro
111117
This will copy `queue_debouncer.php` to your config directory.
112118

113119
### 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.
115120

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 (‘:’)
121+
To identify the job being debounced, the package generates a unique key in the cache for each job type.
122+
123+
Two cache key providers are included:
124+
125+
- `Mpbarlow\LaravelQueueDebouncer\Support\CacheKeyProvider` (default): uses the config's `cache_prefix` value with either: the fully-qualified class name for class-based jobs; or a SHA1 hash of the closure for closure jobs.
126+
- `Mpbarlow\LaravelQueueDebouncer\Support\SerializingCacheKeyProvider`: uses the config's `cache_prefix` value with a SHA1 hash of the serialized job. If you want to debounce jobs based on factors beyond their class name (for example, some internal state), this is the provider to use. This is also required if you need to debounce chains, as the default provider will debounce _all_ chains dispatched by your application as if they are the same job, regardless of what jobs are contained within.
127+
128+
Alternatively, you can provide your own class or closure to cover any other behaviour you might need:
120129

121-
If the default behaviour works for you, but you’d like to use a different cache prefix, you may simply override that value.
130+
If you provide a class, it should 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.
122131

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.
132+
Class-based providers may be globally registered as the default provider by changing the `cache_key_provider` value in the config. Alternatively, you may "hot-swap" the provider using the `usingUniqueIdentifierProvider` method:
124133

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.
134+
```php
135+
$debouncer
136+
->usingCacheKeyProvider(new CustomCacheKeyProvider())
137+
->debounce($job, 10);
138+
```
126139

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.
140+
If you provide a closure, it may only be be hot-swapped:
141+
142+
```php
143+
$debouncer
144+
->usingCacheKeyProvider(fn () => 'my custom key')
145+
->debounce($job, 10);
146+
```
147+
148+
Closure providers automatically have their value prefixed by the configured `cache_prefix`. To override this behaviour, implement a class-based provider that accepts a closure in its constructor, then calls it in its `getKey` method.
128149

129150
### 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.
131151

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`.
152+
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 of the current job, the debouncer knows that no more recent instances of the job have been dispatched, and therefore it is safe to dispatch it.
153+
154+
The default implementation produces a UUID v4 for each dispatch. If you need to override this you may do so in the same manner as cache key providers, globally registering a class under the `unique_identifier_provider` key in the config, or hot-swapping using the `usingUniqueIdentifierProvider` method:
155+
156+
```php
157+
$debouncer
158+
->usingUniqueIdentifierProvider(new CustomUniqueIdentifierProvider())
159+
->debounce($job, 10);
160+
161+
$debouncer
162+
->usingUniqueIdentifierProvider(fn () => 'my custom identifier')
163+
->debounce($job, 10);
164+
```
165+
166+
Class-based providers should implement `Mpbarlow\LaravelQueueDebouncer\Contracts\UniqueIdentifierProvider`.
133167

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

src/Contracts/CacheKeyProvider.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66

77
use Closure;
8-
use Illuminate\Foundation\Bus\Dispatchable;
98

109
interface CacheKeyProvider
1110
{
1211
/**
13-
* @param Dispatchable|Closure $job
12+
* @param \Illuminate\Foundation\Bus\Dispatchable|\Illuminate\Foundation\Bus\PendingChain|Closure $job
1413
* @return string
1514
*/
1615
public function getKey($job): string;

src/Debouncer.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55

66

77
use Closure;
8-
use DateInterval;
9-
use DateTimeInterface;
108
use Illuminate\Bus\Queueable;
119
use Illuminate\Foundation\Bus\Dispatchable;
12-
use Illuminate\Foundation\Bus\PendingDispatch;
1310
use Illuminate\Support\Facades\Cache;
1411
use Mpbarlow\LaravelQueueDebouncer\Contracts\CacheKeyProvider;
1512
use Mpbarlow\LaravelQueueDebouncer\Contracts\UniqueIdentifierProvider;
13+
use Mpbarlow\LaravelQueueDebouncer\Support\ClosureCacheKeyProvider;
14+
use Mpbarlow\LaravelQueueDebouncer\Support\ClosureUniqueIdentifierProvider;
1615

1716
use function dispatch;
1817

@@ -35,9 +34,35 @@ public function __construct(
3534
}
3635

3736
/**
38-
* @param Dispatchable|Closure $job
39-
* @param DateTimeInterface|DateInterval|int|null $wait
40-
* @return PendingDispatch
37+
* @param CacheKeyProvider|Closure $provider
38+
* @return $this
39+
*/
40+
public function usingCacheKeyProvider($provider): self
41+
{
42+
$this->keyProvider = $provider instanceof Closure
43+
? new ClosureCacheKeyProvider($provider)
44+
: $provider;
45+
46+
return $this;
47+
}
48+
49+
/**
50+
* @param UniqueIdentifierProvider|Closure $provider
51+
* @return $this
52+
*/
53+
public function usingUniqueIdentifierProvider($provider): self
54+
{
55+
$this->idProvider = $provider instanceof Closure
56+
? new ClosureUniqueIdentifierProvider($provider)
57+
: $provider;
58+
59+
return $this;
60+
}
61+
62+
/**
63+
* @param Dispatchable|\Illuminate\Foundation\Bus\PendingChain|Closure $job
64+
* @param \DateTimeInterface|\DateInterval|int|null $wait
65+
* @return \Illuminate\Foundation\Bus\PendingDispatch
4166
*/
4267
public function __invoke($job, $wait)
4368
{
@@ -52,9 +77,9 @@ public function __invoke($job, $wait)
5277
}
5378

5479
/**
55-
* @param Dispatchable|Closure $job
56-
* @param DateTimeInterface|DateInterval|int|null $wait
57-
* @return PendingDispatch
80+
* @param Dispatchable|\Illuminate\Foundation\Bus\PendingChain|Closure $job
81+
* @param \DateTimeInterface|\DateInterval|int|null $wait
82+
* @return \Illuminate\Foundation\Bus\PendingDispatch
5883
*/
5984
public function debounce($job, $wait)
6085
{

src/DispatcherFactory.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
use Closure;
8-
use Illuminate\Foundation\Bus\Dispatchable;
8+
use Illuminate\Foundation\Bus\PendingChain;
99
use Illuminate\Support\Facades\Cache;
1010

1111
use function dispatch;
@@ -18,7 +18,7 @@ class DispatcherFactory
1818
* If we used a class as the dispatcher, we would have to check whether the job is a closure ourselves, and
1919
* serialise it if it was.
2020
*
21-
* @param Dispatchable|Closure $job
21+
* @param \Illuminate\Foundation\Bus\Dispatchable|PendingChain|Closure $job
2222
* @param string $key
2323
* @param string $identifier
2424
* @return Closure
@@ -29,7 +29,11 @@ public function makeDispatcher($job, $key, $identifier): Closure
2929
if (Cache::get($key) == $identifier) {
3030
Cache::forget($key);
3131

32-
dispatch($job);
32+
if ($job instanceof PendingChain) {
33+
$job->dispatch();
34+
} else {
35+
dispatch($job);
36+
}
3337
}
3438
};
3539
}

src/Facade/Debouncer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
/**
1010
* @see \Mpbarlow\LaravelQueueDebouncer\Debouncer
11-
* @method static \Illuminate\Foundation\Bus\PendingDispatch debounce(\Illuminate\Foundation\Bus\Dispatchable|\Closure $job, \DateTimeInterface|\DateInterval|int|null $wait)
11+
* @method static \Illuminate\Foundation\Bus\PendingDispatch debounce(\Illuminate\Foundation\Bus\Dispatchable|\Illuminate\Foundation\Bus\PendingChain|\Closure $job, \DateTimeInterface|\DateInterval|int|null $wait)
1212
*/
1313
class Debouncer extends Facade
1414
{
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
4+
namespace Mpbarlow\LaravelQueueDebouncer\Support;
5+
6+
7+
use Closure;
8+
use Mpbarlow\LaravelQueueDebouncer\Contracts\CacheKeyProvider as CacheKeyProviderContract;
9+
10+
use function config;
11+
12+
class ClosureCacheKeyProvider implements CacheKeyProviderContract
13+
{
14+
protected $provider;
15+
16+
public function __construct(Closure $provider)
17+
{
18+
$this->provider = $provider;
19+
}
20+
21+
public function getKey($job): string
22+
{
23+
return config('queue_debouncer.cache_prefix') . ':' . ($this->provider)($job);
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
4+
namespace Mpbarlow\LaravelQueueDebouncer\Support;
5+
6+
7+
use Closure;
8+
use Mpbarlow\LaravelQueueDebouncer\Contracts\UniqueIdentifierProvider as UniqueIdentifierProviderContract;
9+
10+
class ClosureUniqueIdentifierProvider implements UniqueIdentifierProviderContract
11+
{
12+
protected $provider;
13+
14+
public function __construct(Closure $provider)
15+
{
16+
$this->provider = $provider;
17+
}
18+
19+
public function getIdentifier(): string
20+
{
21+
return ($this->provider)();
22+
}
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
4+
namespace Mpbarlow\LaravelQueueDebouncer\Support;
5+
6+
7+
use Mpbarlow\LaravelQueueDebouncer\Contracts\CacheKeyProvider as CacheKeyProviderContract;
8+
9+
use function config;
10+
use function sha1;
11+
12+
class SerializingCacheKeyProvider implements CacheKeyProviderContract
13+
{
14+
public function getKey($job): string
15+
{
16+
return config('queue_debouncer.cache_prefix') . ':' . sha1(\Opis\Closure\serialize($job));
17+
}
18+
}

src/Support/helpers.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
<?php
22

3-
use Illuminate\Foundation\Bus\Dispatchable;
4-
use Illuminate\Foundation\Bus\PendingDispatch;
53
use Mpbarlow\LaravelQueueDebouncer\Debouncer;
64

75
if (! function_exists('debounce')) {
86
/**
9-
* @param Dispatchable|Closure $job
7+
* @param \Illuminate\Foundation\Bus\Dispatchable|\Illuminate\Foundation\Bus\PendingChain|Closure $job
108
* @param DateTimeInterface|DateInterval|int|null $wait
11-
* @return PendingDispatch
9+
* @return \Illuminate\Foundation\Bus\PendingDispatch
1210
*/
1311
function debounce($job, $wait)
1412
{

0 commit comments

Comments
 (0)