I ran into a non-intuitive issue the other day with some queued jobs: after being dispatched, multiple jobs were immediately failing with a MaxAttemptsExceededException
error.
This specific job class happens to use the WithoutOverlapping
trait so it only runs a single concurrent job per order to prevent race conditions. However, this job can be dispatched multiple times per order for different travelers. Here’s a simplified version of the job:
Can you spot the issue?
When this job was dispatched for order 123 for the only traveler, the queue workers would pick up the job and start processing and everything would be fine.
When this job was dispatched for order 124 for traveler 1 and again for traveler 2, a queue worker would pick up the traveler 1 job and start processing it. Another queue worker would pick up the traveler 2 job, check the middleware, see that another job was already being processed for the order, and fail the job to release it back onto the queue to be tried again later.
Because the first job could take a little while to run, another queue worker would then pick up the traveler 2 job again and again immediately fail it. This would happen until it reached the max number of attempts for the job, and then it would fail throwing a MaxAttemptsExceededException
. Depending on how busy the queue workers were, this would happen within a second of the job being dispatched.
It took me a little bit to figure out why jobs were immediately failing with that exception but once I did it made total sense. 🤦🏻♂️
The fix? Simple: add the traveler number into the WithoutOverlapping
key: new WithoutOverlapping($this->order->id.'-'.$this-travelerNumber)
.
TL;DR: make sure you use actually-unique WithoutOverlapping
keys to prevent immediate MaxAttemptsExceededException
after dispatching jobs.