Lets learn how to throttle jobs that throws exceptions by using a same third-party service instead of throttle jobs separately that cause extra exceptions for the same third-party service.
Default behavior of using ThrottleExceptions Middleware
There are 3 job classes that access the same third-party service. All 3 job classes uses ThrottleExceptions middleware.
- Laravel create 3 different buckets to track exceptions and to throttle the job execution. Those 3 buckets are uniquely identified based job class.
- Imagine the third-party service is not working.
- Then all 3 jobs throws exceptions.
- Job throttling happens separately for all 3 jobs for using the same third-party service.
- If throttle after 10 attempts all three jobs create 30 attempts before throttling.
With using same key on by() method
- There are 3 jobs with same key used inside the by() method with ThrottleExceptions middleware. Set throttle after 2 attempts and rest 60 seconds. (ex: ThrottleExceptions(2, 60) ) Simultaneously received a dispatch requests for all three jobs.
- All three job execute once and wait for rest duration. Because maximum attempts before throttling is passed for the bucket.
- Even max attempts are 2 does not mean third job to stop processing. If there is 5 jobs and max attempts before throttling are 2, then 5 jobs execute and wait for rest duration.
- Sharing a same bucket (same by() key) mean sharing the same attempts before throttling. Not waiting for job that throws exception to succeed to process others.
Example
Lets test ThrottleExceptions middleware and how it work with a real world example.
Task: Create 3 job classes and set maximum 5 attempts before throttling. Set 30 seconds for rest duration for throttling. Create a third-party service that throws exception for first 50 seconds of accessing that service. Use that service to fail above created jobs. Log required information.
Setup
Setup a fresh Laravel application. Refer official Laravel documentation to setup your project. I am not covering that part since it is not necessary for this example. Don’t use default SQLite database because it throw errors. Make sure to use MySQL database.
Create Job Classes
Lets create three job classes as JobTaskOne, JobTaskTwo, JobTaskThree using below artisan command.
php artisan make:job JobTaskOne
php artisan make:job JobTaskTwo
php artisan make:job JobTaskThree
Create a third party service that throws exceptions
Create a directory called Services inside the app directory. Create DataService.php inside the Services directory.
- Accept how long this service should throws exceptions via the constructor method.
- Check current time and give time via constructor in order to throw exceptions.
namespace App\Services;
use DateTime;
use Exception;
class DataService{
private DateTime $failUntil;
function __construct(DateTime $time) {
$this->failUntil = $time;
}
public function handle()
{
//Throws exceptions until the current time passes
//the give time. It may be now() + 30 seconds, etc.
if(now()->lessThan($this->failUntil)){
throw new Exception('Custom Exception');
}
}
}
Add ThrottleExceptions middleware with same key
Open jobTaskOne class
- Create an instance of the DataService class inside the constructor by using 50 seconds date time object.
- Add 5 seconds sleep to simulate time consuming job task.
- Call DataService to throws exceptions in the handle() method of the job class.
- Add retryUntil method to stop execution of this job after given period of time if it failed to complete its task. (It is a good practice to include retryUntil() )
<?php
namespace App\Jobs;
use App\Services\DataService;
use DateTime;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Support\Facades\Log;
class JobTaskOne implements ShouldQueue
{
use Queueable;
private $thirdPartyService;
/**
* Create a new job instance.
*/
public function __construct()
{
$this->thirdPartyService = new DataService(now()->addSeconds(50));
}
/**
* Execute the job.
*/
public function handle(): void
{
Log::info('Processing.... job one');
sleep(5);
$this->thirdPartyService->handle();
Log::info("Completed Job one");
}
public function middleware():array
{
return [
(new ThrottlesExceptions(5,30))->by('dataService')
];
}
//Stop retrying after one minutes
public function retryUntil():DateTime
{
return now()->addMinutes(5);
}
}
Do the same for JobTaskTwo and JobTaskThree classes.
Dispatch job classes
Modify web.php file. Dispatch all three job classes within the root route of the application.
<?php
use App\Jobs\JobTaskOne;
use App\Jobs\JobTaskThree;
use App\Jobs\JobTaskTwo;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
Log::info("Received request for job one");
JobTaskOne::dispatch();
Log::info("Received request for job two");
JobTaskTwo::dispatch();
Log::info("Received request for job three");
JobTaskThree::dispatch();
});
Run your Laravel application using artisan command.
php artisan serve
Run multiple queue workers by running below artisan command on three terminals.
php artisan queue:work --tries=20
Check Laravel log file.
[2024-12-21 03:04:01] local.INFO: Received request for job one
[2024-12-21 03:04:01] local.INFO: Received request for job two
[2024-12-21 03:04:01] local.INFO: Received request for job three
# Excute 5 times before throttling
[2024-12-21 03:04:02] local.INFO: Processing.... job one
[2024-12-21 03:04:02] local.INFO: Processing.... job two
[2024-12-21 03:04:02] local.INFO: Processing.... job three
[2024-12-21 03:04:02] local.INFO: Processing.... job one
[2024-12-21 03:04:02] local.INFO: Processing.... job two
# Excute 5 times before throttling
[2024-12-21 03:04:35] local.INFO: Processing.... job three
[2024-12-21 03:04:35] local.INFO: Processing.... job one
[2024-12-21 03:04:35] local.INFO: Processing.... job two
[2024-12-21 03:04:35] local.INFO: Processing.... job three
[2024-12-21 03:04:35] local.INFO: Processing.... job one
[2024-12-21 03:05:08] local.INFO: Processing.... job two
[2024-12-21 03:05:08] local.INFO: Completed Job two
[2024-12-21 03:05:08] local.INFO: Processing.... job three
[2024-12-21 03:05:08] local.INFO: Completed Job three
[2024-12-21 03:05:08] local.INFO: Processing.... job one
[2024-12-21 03:05:08] local.INFO: Completed Job one
According to above log information you will understand how this middleware work with by() method. even though each job class has 5 attempts it does not able to utilize all 5 attempts since every job class share their attempts with other job classes.