[13.x] Add debounceable queued jobs#59507
Conversation
|
Thanks for submitting a PR! Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface. Pull requests that are abandoned in draft may be closed due to inactivity. |
1f1a4e9 to
02c8246
Compare
|
Good stuff. The only thing i dont like is the empty interface and the |
|
@bert-w thanks for taking the time to review the code, both points you mentioned are consistent with Laravel and the similar feature |
|
@taylorotwell any thoughts on this? happy to make adjustments as needed |
|
Something that I missed, and I didn't even know it. I would love to see something like this added to the Framework. I just noticed in |
|
@kohlerdominik retries shouldn't be a problem, if it gets bounced back into the queue it will re-check whether it is still the owner of this job and either continue processing or abort (if it's no longer the owner) |
|
I feel like there is too much going on here. Interface, attribute, middleware - I'm not sure which one to use. Just have an attribute imo. |
9495bb4 to
a2c715e
Compare
|
@taylorotwell No problem, PR updated with your review, should be significantly simpler now |
Adds last-dispatch-wins semantics for queued jobs via ShouldBeDebounced interface. When multiple dispatches occur for the same debounce identity, only the most recent executes. New files: - ShouldBeDebounced marker interface - DebounceLock cache-based lock manager - DebounceFor PHP 8 attribute - JobDebounced event - Debounced standalone middleware Framework integration: - PendingDispatch acquires debounce lock at dispatch time - CallQueuedHandler checks ownership at execution time - Queue/SyncQueue register transaction rollback handlers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Guard ensureDebounceLockIsReleased() call with instanceof ShouldBeDebounced to prevent extra isReleased() calls breaking ThrottlesExceptionsTest mocks - Extend debounce lock TTL to debounceFor*2 so lock remains valid when the delayed job becomes available for processing - Travel past debounce window in DebouncedJobTest before running queue worker so delayed jobs are available on async queue drivers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Make release() owner-aware to prevent wiping a newer dispatch's lock - Fail-open when lock is missing (cache eviction/TTL expiry) instead of silently deleting the job - Continue chain/batch dispatch when a debounced job is superseded - Release debounce lock via context on model-not-found exceptions - Fire JobDebounced event from Debounced middleware for parity with the ShouldBeDebounced interface path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The lock TTL must exceed the debounce delay so the ownership check can distinguish superseded jobs from expired locks at execution time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Locks are the wrong abstraction for debounce — we need a "latest owner" marker, not mutual exclusion. This replaces forceRelease/get/restoreLock with simple cache put/get/forget operations. The TTL is now generous (10x debounceFor, min 300s) for garbage collection only — correctness no longer depends on it. A 15-minute debounce window no longer requires a 30-minute lock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skip delayed-job tests on Beanstalkd (travelTo cannot control an external server's delay timer) - Replace Event::fake with Event::listen for the superseded event test to avoid subtle interaction issues with queue:work artisan command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Releasing the token after the current owner executes creates a race: if the current job processes before a superseded one, removing the token causes the superseded job to see an empty cache and execute via fail-open. The token's only purpose is supersession detection. Let the generous GC TTL (min 300s) handle cleanup. Transaction rollback callbacks still release the token when a dispatch is abandoned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simplify to attribute-only approach per maintainer feedback. The #[DebounceFor] attribute is now the sole mechanism for enabling job debouncing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move debounceOwner property to Queueable trait to avoid PHP 8.2+ dynamic property deprecations, extract getDebounceDelay() on DebounceLock to eliminate duplicated resolution logic in PendingDispatch, and fix stale comment about token lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoid resolving Cache from the container when the job has no debounce configuration, fixing BindingResolutionException in test environments without a cache store bound. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6b98962 to
a90e0a3
Compare
…ehavior - dispatch(new Job)->debounceFor(30) enables debouncing at the call site - #[DebounceFor(30, maxWait: 120)] caps how long a job can be deferred - Superseded jobs no longer trigger chain dispatch or batch success recording - Add test coverage for debounceVia() custom cache store
Summary
Adds debouncing for queued jobs — when the same job is dispatched multiple times within a time window, only the last dispatch executes. Earlier dispatches are silently discarded at execution time.
Apply the
#[DebounceFor]attribute to anyShouldQueuejob, or debounce at the dispatch site. No interface needed.How it works
JobDebouncedevent fires.cache:clear), the job executes rather than being silently lost.Uses plain cache
put/get— not locks. Locks enforce mutual exclusion (first-writer-wins), which is the opposite of what debounce needs.Comparison with
ShouldBeUniqueShouldBeUnique#[DebounceFor]UniqueLockDebounceLockCombining both on the same job throws
LogicException— first-wins and last-wins are mutually exclusive.Usage
Attribute-based (on the job class)
Call-site debouncing
Debounce any job at the dispatch site without modifying the job class:
Max-wait cap
Prevent a frequently re-dispatched job from being deferred indefinitely. After
maxWaitseconds from the first dispatch, the job executes immediately:Override delay via method
The
debounceFor()method takes precedence over the attribute value:Custom cache store
Listen for superseded jobs
Design notes
DebounceLock::release()only removes the token if it still belongs to the caller, preventing a finished job from wiping a newer dispatch's token.JobDebouncedand is deleted — it does not dispatch chain links or record batch success, since no work was performed.ShouldBeUniquepattern.->debounceFor()>debounceFor()method >#[DebounceFor]attribute.