6556 lines
158 KiB
Markdown
6556 lines
158 KiB
Markdown
# Queues
|
||
|
||
* Introduction
|
||
* Connections vs. Queues
|
||
* Driver Notes and Prerequisites
|
||
* Creating Jobs
|
||
* Generating Job Classes
|
||
* Class Structure
|
||
* Unique Jobs
|
||
* Encrypted Jobs
|
||
* Job Middleware
|
||
* Rate Limiting
|
||
* Preventing Job Overlaps
|
||
* Throttling Exceptions
|
||
* Skipping Jobs
|
||
* Dispatching Jobs
|
||
* Delayed Dispatching
|
||
* Synchronous Dispatching
|
||
* Jobs & Database Transactions
|
||
* Job Chaining
|
||
* Customizing The Queue and Connection
|
||
* Specifying Max Job Attempts / Timeout Values
|
||
* Error Handling
|
||
* Job Batching
|
||
* Defining Batchable Jobs
|
||
* Dispatching Batches
|
||
* Chains and Batches
|
||
* Adding Jobs to Batches
|
||
* Inspecting Batches
|
||
* Cancelling Batches
|
||
* Batch Failures
|
||
* Pruning Batches
|
||
* Storing Batches in DynamoDB
|
||
* Queueing Closures
|
||
* Running the Queue Worker
|
||
* The `queue:work` Command
|
||
* Queue Priorities
|
||
* Queue Workers and Deployment
|
||
* Job Expirations and Timeouts
|
||
* Supervisor Configuration
|
||
* Dealing With Failed Jobs
|
||
* Cleaning Up After Failed Jobs
|
||
* Retrying Failed Jobs
|
||
* Ignoring Missing Models
|
||
* Pruning Failed Jobs
|
||
* Storing Failed Jobs in DynamoDB
|
||
* Disabling Failed Job Storage
|
||
* Failed Job Events
|
||
* Clearing Jobs From Queues
|
||
* Monitoring Your Queues
|
||
* Testing
|
||
* Faking a Subset of Jobs
|
||
* Testing Job Chains
|
||
* Testing Job Batches
|
||
* Testing Job / Queue Interactions
|
||
* Job Events
|
||
|
||
## Introduction
|
||
|
||
While building your web application, you may have some tasks, such as parsing
|
||
and storing an uploaded CSV file, that take too long to perform during a
|
||
typical web request. Thankfully, Laravel allows you to easily create queued
|
||
jobs that may be processed in the background. By moving time intensive tasks
|
||
to a queue, your application can respond to web requests with blazing speed
|
||
and provide a better user experience to your customers.
|
||
|
||
Laravel queues provide a unified queueing API across a variety of different
|
||
queue backends, such as [Amazon SQS](https://aws.amazon.com/sqs/),
|
||
[Redis](https://redis.io), or even a relational database.
|
||
|
||
Laravel's queue configuration options are stored in your application's
|
||
`config/queue.php` configuration file. In this file, you will find connection
|
||
configurations for each of the queue drivers that are included with the
|
||
framework, including the database, [Amazon SQS](https://aws.amazon.com/sqs/),
|
||
[Redis](https://redis.io), and [Beanstalkd](https://beanstalkd.github.io/)
|
||
drivers, as well as a synchronous driver that will execute jobs immediately
|
||
(for use during development or testing). A `null` queue driver is also
|
||
included which discards queued jobs.
|
||
|
||
Laravel Horizon is a beautiful dashboard and configuration system for your
|
||
Redis powered queues. Check out the full [Horizon
|
||
documentation](/docs/12.x/horizon) for more information.
|
||
|
||
### Connections vs. Queues
|
||
|
||
Before getting started with Laravel queues, it is important to understand the
|
||
distinction between "connections" and "queues". In your `config/queue.php`
|
||
configuration file, there is a `connections` configuration array. This option
|
||
defines the connections to backend queue services such as Amazon SQS,
|
||
Beanstalk, or Redis. However, any given queue connection may have multiple
|
||
"queues" which may be thought of as different stacks or piles of queued jobs.
|
||
|
||
Note that each connection configuration example in the `queue` configuration
|
||
file contains a `queue` attribute. This is the default queue that jobs will be
|
||
dispatched to when they are sent to a given connection. In other words, if you
|
||
dispatch a job without explicitly defining which queue it should be dispatched
|
||
to, the job will be placed on the queue that is defined in the `queue`
|
||
attribute of the connection configuration:
|
||
|
||
|
||
|
||
1use App\Jobs\ProcessPodcast;
|
||
|
||
2
|
||
|
||
3// This job is sent to the default connection's default queue...
|
||
|
||
4ProcessPodcast::dispatch();
|
||
|
||
5
|
||
|
||
6// This job is sent to the default connection's "emails" queue...
|
||
|
||
7ProcessPodcast::dispatch()->onQueue('emails');
|
||
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
|
||
// This job is sent to the default connection's default queue...
|
||
ProcessPodcast::dispatch();
|
||
|
||
// This job is sent to the default connection's "emails" queue...
|
||
ProcessPodcast::dispatch()->onQueue('emails');
|
||
|
||
Some applications may not need to ever push jobs onto multiple queues, instead
|
||
preferring to have one simple queue. However, pushing jobs to multiple queues
|
||
can be especially useful for applications that wish to prioritize or segment
|
||
how jobs are processed, since the Laravel queue worker allows you to specify
|
||
which queues it should process by priority. For example, if you push jobs to a
|
||
`high` queue, you may run a worker that gives them higher processing priority:
|
||
|
||
|
||
|
||
1php artisan queue:work --queue=high,default
|
||
|
||
|
||
php artisan queue:work --queue=high,default
|
||
|
||
### Driver Notes and Prerequisites
|
||
|
||
#### Database
|
||
|
||
In order to use the `database` queue driver, you will need a database table to
|
||
hold the jobs. Typically, this is included in Laravel's default
|
||
`0001_01_01_000002_create_jobs_table.php` [database
|
||
migration](/docs/12.x/migrations); however, if your application does not
|
||
contain this migration, you may use the `make:queue-table` Artisan command to
|
||
create it:
|
||
|
||
|
||
|
||
1php artisan make:queue-table
|
||
|
||
2
|
||
|
||
3php artisan migrate
|
||
|
||
|
||
php artisan make:queue-table
|
||
|
||
php artisan migrate
|
||
|
||
#### Redis
|
||
|
||
In order to use the `redis` queue driver, you should configure a Redis
|
||
database connection in your `config/database.php` configuration file.
|
||
|
||
The `serializer` and `compression` Redis options are not supported by the
|
||
`redis` queue driver.
|
||
|
||
##### Redis Cluster
|
||
|
||
If your Redis queue connection uses a [Redis
|
||
Cluster](https://redis.io/docs/latest/operate/rs/databases/durability-
|
||
ha/clustering), your queue names must contain a [key hash
|
||
tag](https://redis.io/docs/latest/develop/using-commands/keyspace/#hashtags).
|
||
This is required in order to ensure all of the Redis keys for a given queue
|
||
are placed into the same hash slot:
|
||
|
||
|
||
|
||
1'redis' => [
|
||
|
||
2 'driver' => 'redis',
|
||
|
||
3 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||
|
||
4 'queue' => env('REDIS_QUEUE', '{default}'),
|
||
|
||
5 'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||
|
||
6 'block_for' => null,
|
||
|
||
7 'after_commit' => false,
|
||
|
||
8],
|
||
|
||
|
||
'redis' => [
|
||
'driver' => 'redis',
|
||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||
'queue' => env('REDIS_QUEUE', '{default}'),
|
||
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||
'block_for' => null,
|
||
'after_commit' => false,
|
||
],
|
||
|
||
##### Blocking
|
||
|
||
When using the Redis queue, you may use the `block_for` configuration option
|
||
to specify how long the driver should wait for a job to become available
|
||
before iterating through the worker loop and re-polling the Redis database.
|
||
|
||
Adjusting this value based on your queue load can be more efficient than
|
||
continually polling the Redis database for new jobs. For instance, you may set
|
||
the value to `5` to indicate that the driver should block for five seconds
|
||
while waiting for a job to become available:
|
||
|
||
|
||
|
||
1'redis' => [
|
||
|
||
2 'driver' => 'redis',
|
||
|
||
3 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||
|
||
4 'queue' => env('REDIS_QUEUE', 'default'),
|
||
|
||
5 'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||
|
||
6 'block_for' => 5,
|
||
|
||
7 'after_commit' => false,
|
||
|
||
8],
|
||
|
||
|
||
'redis' => [
|
||
'driver' => 'redis',
|
||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||
'queue' => env('REDIS_QUEUE', 'default'),
|
||
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||
'block_for' => 5,
|
||
'after_commit' => false,
|
||
],
|
||
|
||
Setting `block_for` to `0` will cause queue workers to block indefinitely
|
||
until a job is available. This will also prevent signals such as `SIGTERM`
|
||
from being handled until the next job has been processed.
|
||
|
||
#### Other Driver Prerequisites
|
||
|
||
The following dependencies are needed for the listed queue drivers. These
|
||
dependencies may be installed via the Composer package manager:
|
||
|
||
* Amazon SQS: `aws/aws-sdk-php ~3.0`
|
||
* Beanstalkd: `pda/pheanstalk ~5.0`
|
||
* Redis: `predis/predis ~2.0` or phpredis PHP extension
|
||
* [MongoDB](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/queues/): `mongodb/laravel-mongodb`
|
||
|
||
## Creating Jobs
|
||
|
||
### Generating Job Classes
|
||
|
||
By default, all of the queueable jobs for your application are stored in the
|
||
`app/Jobs` directory. If the `app/Jobs` directory doesn't exist, it will be
|
||
created when you run the `make:job` Artisan command:
|
||
|
||
|
||
|
||
1php artisan make:job ProcessPodcast
|
||
|
||
|
||
php artisan make:job ProcessPodcast
|
||
|
||
The generated class will implement the
|
||
`Illuminate\Contracts\Queue\ShouldQueue` interface, indicating to Laravel that
|
||
the job should be pushed onto the queue to run asynchronously.
|
||
|
||
Job stubs may be customized using [stub publishing](/docs/12.x/artisan#stub-
|
||
customization).
|
||
|
||
### Class Structure
|
||
|
||
Job classes are very simple, normally containing only a `handle` method that
|
||
is invoked when the job is processed by the queue. To get started, let's take
|
||
a look at an example job class. In this example, we'll pretend we manage a
|
||
podcast publishing service and need to process the uploaded podcast files
|
||
before they are published:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use App\Models\Podcast;
|
||
|
||
6use App\Services\AudioProcessor;
|
||
|
||
7use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
8use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
9
|
||
|
||
10class ProcessPodcast implements ShouldQueue
|
||
|
||
11{
|
||
|
||
12 use Queueable;
|
||
|
||
13
|
||
|
||
14 /**
|
||
|
||
15 * Create a new job instance.
|
||
|
||
16 */
|
||
|
||
17 public function __construct(
|
||
|
||
18 public Podcast $podcast,
|
||
|
||
19 ) {}
|
||
|
||
20
|
||
|
||
21 /**
|
||
|
||
22 * Execute the job.
|
||
|
||
23 */
|
||
|
||
24 public function handle(AudioProcessor $processor): void
|
||
|
||
25 {
|
||
|
||
26 // Process uploaded podcast...
|
||
|
||
27 }
|
||
|
||
28}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Models\Podcast;
|
||
use App\Services\AudioProcessor;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
public Podcast $podcast,
|
||
) {}
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(AudioProcessor $processor): void
|
||
{
|
||
// Process uploaded podcast...
|
||
}
|
||
}
|
||
|
||
In this example, note that we were able to pass an [Eloquent
|
||
model](/docs/12.x/eloquent) directly into the queued job's constructor.
|
||
Because of the `Queueable` trait that the job is using, Eloquent models and
|
||
their loaded relationships will be gracefully serialized and unserialized when
|
||
the job is processing.
|
||
|
||
If your queued job accepts an Eloquent model in its constructor, only the
|
||
identifier for the model will be serialized onto the queue. When the job is
|
||
actually handled, the queue system will automatically re-retrieve the full
|
||
model instance and its loaded relationships from the database. This approach
|
||
to model serialization allows for much smaller job payloads to be sent to your
|
||
queue driver.
|
||
|
||
#### `handle` Method Dependency Injection
|
||
|
||
The `handle` method is invoked when the job is processed by the queue. Note
|
||
that we are able to type-hint dependencies on the `handle` method of the job.
|
||
The Laravel [service container](/docs/12.x/container) automatically injects
|
||
these dependencies.
|
||
|
||
If you would like to take total control over how the container injects
|
||
dependencies into the `handle` method, you may use the container's
|
||
`bindMethod` method. The `bindMethod` method accepts a callback which receives
|
||
the job and the container. Within the callback, you are free to invoke the
|
||
`handle` method however you wish. Typically, you should call this method from
|
||
the `boot` method of your `App\Providers\AppServiceProvider` [service
|
||
provider](/docs/12.x/providers):
|
||
|
||
|
||
|
||
1use App\Jobs\ProcessPodcast;
|
||
|
||
2use App\Services\AudioProcessor;
|
||
|
||
3use Illuminate\Contracts\Foundation\Application;
|
||
|
||
4
|
||
|
||
5$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
|
||
|
||
6 return $job->handle($app->make(AudioProcessor::class));
|
||
|
||
7});
|
||
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Services\AudioProcessor;
|
||
use Illuminate\Contracts\Foundation\Application;
|
||
|
||
$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
|
||
return $job->handle($app->make(AudioProcessor::class));
|
||
});
|
||
|
||
Binary data, such as raw image contents, should be passed through the
|
||
`base64_encode` function before being passed to a queued job. Otherwise, the
|
||
job may not properly serialize to JSON when being placed on the queue.
|
||
|
||
#### Queued Relationships
|
||
|
||
Because all loaded Eloquent model relationships also get serialized when a job
|
||
is queued, the serialized job string can sometimes become quite large.
|
||
Furthermore, when a job is deserialized and model relationships are re-
|
||
retrieved from the database, they will be retrieved in their entirety. Any
|
||
previous relationship constraints that were applied before the model was
|
||
serialized during the job queueing process will not be applied when the job is
|
||
deserialized. Therefore, if you wish to work with a subset of a given
|
||
relationship, you should re-constrain that relationship within your queued
|
||
job.
|
||
|
||
Or, to prevent relations from being serialized, you can call the
|
||
`withoutRelations` method on the model when setting a property value. This
|
||
method will return an instance of the model without its loaded relationships:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Create a new job instance.
|
||
|
||
3 */
|
||
|
||
4public function __construct(
|
||
|
||
5 Podcast $podcast,
|
||
|
||
6) {
|
||
|
||
7 $this->podcast = $podcast->withoutRelations();
|
||
|
||
8}
|
||
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
Podcast $podcast,
|
||
) {
|
||
$this->podcast = $podcast->withoutRelations();
|
||
}
|
||
|
||
If you are using [PHP constructor property
|
||
promotion](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion)
|
||
and would like to indicate that an Eloquent model should not have its
|
||
relations serialized, you may use the `WithoutRelations` attribute:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Attributes\WithoutRelations;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Create a new job instance.
|
||
|
||
5 */
|
||
|
||
6public function __construct(
|
||
|
||
7 #[WithoutRelations]
|
||
|
||
8 public Podcast $podcast,
|
||
|
||
9) {}
|
||
|
||
|
||
use Illuminate\Queue\Attributes\WithoutRelations;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
#[WithoutRelations]
|
||
public Podcast $podcast,
|
||
) {}
|
||
|
||
For convenience, if you wish to serialize all models without relationships,
|
||
you may apply the `WithoutRelations` attribute to the entire class instead of
|
||
applying the attribute to each model:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use App\Models\DistributionPlatform;
|
||
|
||
6use App\Models\Podcast;
|
||
|
||
7use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
8use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
9use Illuminate\Queue\Attributes\WithoutRelations;
|
||
|
||
10
|
||
|
||
11#[WithoutRelations]
|
||
|
||
12class ProcessPodcast implements ShouldQueue
|
||
|
||
13{
|
||
|
||
14 use Queueable;
|
||
|
||
15
|
||
|
||
16 /**
|
||
|
||
17 * Create a new job instance.
|
||
|
||
18 */
|
||
|
||
19 public function __construct(
|
||
|
||
20 public Podcast $podcast,
|
||
|
||
21 public DistributionPlatform $platform,
|
||
|
||
22 ) {}
|
||
|
||
23}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Models\DistributionPlatform;
|
||
use App\Models\Podcast;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
use Illuminate\Queue\Attributes\WithoutRelations;
|
||
|
||
#[WithoutRelations]
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
public Podcast $podcast,
|
||
public DistributionPlatform $platform,
|
||
) {}
|
||
}
|
||
|
||
If a job receives a collection or array of Eloquent models instead of a single
|
||
model, the models within that collection will not have their relationships
|
||
restored when the job is deserialized and executed. This is to prevent
|
||
excessive resource usage on jobs that deal with large numbers of models.
|
||
|
||
### Unique Jobs
|
||
|
||
Unique jobs require a cache driver that supports
|
||
[locks](/docs/12.x/cache#atomic-locks). Currently, the `memcached`, `redis`,
|
||
`dynamodb`, `database`, `file`, and `array` cache drivers support atomic
|
||
locks. In addition, unique job constraints do not apply to jobs within
|
||
batches.
|
||
|
||
Sometimes, you may want to ensure that only one instance of a specific job is
|
||
on the queue at any point in time. You may do so by implementing the
|
||
`ShouldBeUnique` interface on your job class. This interface does not require
|
||
you to define any additional methods on your class:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
4use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||
|
||
5
|
||
|
||
6class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
|
||
|
||
7{
|
||
|
||
8 // ...
|
||
|
||
9}
|
||
|
||
|
||
<?php
|
||
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||
|
||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
|
||
{
|
||
// ...
|
||
}
|
||
|
||
In the example above, the `UpdateSearchIndex` job is unique. So, the job will
|
||
not be dispatched if another instance of the job is already on the queue and
|
||
has not finished processing.
|
||
|
||
In certain cases, you may want to define a specific "key" that makes the job
|
||
unique or you may want to specify a timeout beyond which the job no longer
|
||
stays unique. To accomplish this, you may define `uniqueId` and `uniqueFor`
|
||
properties or methods on your job class:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
6use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||
|
||
7
|
||
|
||
8class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
|
||
|
||
9{
|
||
|
||
10 /**
|
||
|
||
11 * The product instance.
|
||
|
||
12 *
|
||
|
||
13 * @var \App\Models\Product
|
||
|
||
14 */
|
||
|
||
15 public $product;
|
||
|
||
16
|
||
|
||
17 /**
|
||
|
||
18 * The number of seconds after which the job's unique lock will be released.
|
||
|
||
19 *
|
||
|
||
20 * @var int
|
||
|
||
21 */
|
||
|
||
22 public $uniqueFor = 3600;
|
||
|
||
23
|
||
|
||
24 /**
|
||
|
||
25 * Get the unique ID for the job.
|
||
|
||
26 */
|
||
|
||
27 public function uniqueId(): string
|
||
|
||
28 {
|
||
|
||
29 return $this->product->id;
|
||
|
||
30 }
|
||
|
||
31}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||
|
||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
|
||
{
|
||
/**
|
||
* The product instance.
|
||
*
|
||
* @var \App\Models\Product
|
||
*/
|
||
public $product;
|
||
|
||
/**
|
||
* The number of seconds after which the job's unique lock will be released.
|
||
*
|
||
* @var int
|
||
*/
|
||
public $uniqueFor = 3600;
|
||
|
||
/**
|
||
* Get the unique ID for the job.
|
||
*/
|
||
public function uniqueId(): string
|
||
{
|
||
return $this->product->id;
|
||
}
|
||
}
|
||
|
||
In the example above, the `UpdateSearchIndex` job is unique by a product ID.
|
||
So, any new dispatches of the job with the same product ID will be ignored
|
||
until the existing job has completed processing. In addition, if the existing
|
||
job is not processed within one hour, the unique lock will be released and
|
||
another job with the same unique key can be dispatched to the queue.
|
||
|
||
If your application dispatches jobs from multiple web servers or containers,
|
||
you should ensure that all of your servers are communicating with the same
|
||
central cache server so that Laravel can accurately determine if a job is
|
||
unique.
|
||
|
||
#### Keeping Jobs Unique Until Processing Begins
|
||
|
||
By default, unique jobs are "unlocked" after a job completes processing or
|
||
fails all of its retry attempts. However, there may be situations where you
|
||
would like your job to unlock immediately before it is processed. To
|
||
accomplish this, your job should implement the `ShouldBeUniqueUntilProcessing`
|
||
contract instead of the `ShouldBeUnique` contract:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
4use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||
|
||
5
|
||
|
||
6class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||
|
||
7{
|
||
|
||
8 // ...
|
||
|
||
9}
|
||
|
||
|
||
<?php
|
||
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||
|
||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
|
||
{
|
||
// ...
|
||
}
|
||
|
||
#### Unique Job Locks
|
||
|
||
Behind the scenes, when a `ShouldBeUnique` job is dispatched, Laravel attempts
|
||
to acquire a [lock](/docs/12.x/cache#atomic-locks) with the `uniqueId` key. If
|
||
the lock is already held, the job is not dispatched. This lock is released
|
||
when the job completes processing or fails all of its retry attempts. By
|
||
default, Laravel will use the default cache driver to obtain this lock.
|
||
However, if you wish to use another driver for acquiring the lock, you may
|
||
define a `uniqueVia` method that returns the cache driver that should be used:
|
||
|
||
|
||
|
||
1use Illuminate\Contracts\Cache\Repository;
|
||
|
||
2use Illuminate\Support\Facades\Cache;
|
||
|
||
3
|
||
|
||
4class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
|
||
|
||
5{
|
||
|
||
6 // ...
|
||
|
||
7
|
||
|
||
8 /**
|
||
|
||
9 * Get the cache driver for the unique job lock.
|
||
|
||
10 */
|
||
|
||
11 public function uniqueVia(): Repository
|
||
|
||
12 {
|
||
|
||
13 return Cache::driver('redis');
|
||
|
||
14 }
|
||
|
||
15}
|
||
|
||
|
||
use Illuminate\Contracts\Cache\Repository;
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
|
||
{
|
||
// ...
|
||
|
||
/**
|
||
* Get the cache driver for the unique job lock.
|
||
*/
|
||
public function uniqueVia(): Repository
|
||
{
|
||
return Cache::driver('redis');
|
||
}
|
||
}
|
||
|
||
If you only need to limit the concurrent processing of a job, use the
|
||
[WithoutOverlapping](/docs/12.x/queues#preventing-job-overlaps) job middleware
|
||
instead.
|
||
|
||
### Encrypted Jobs
|
||
|
||
Laravel allows you to ensure the privacy and integrity of a job's data via
|
||
[encryption](/docs/12.x/encryption). To get started, simply add the
|
||
`ShouldBeEncrypted` interface to the job class. Once this interface has been
|
||
added to the class, Laravel will automatically encrypt your job before pushing
|
||
it onto a queue:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||
|
||
4use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
5
|
||
|
||
6class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
|
||
|
||
7{
|
||
|
||
8 // ...
|
||
|
||
9}
|
||
|
||
|
||
<?php
|
||
|
||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
|
||
{
|
||
// ...
|
||
}
|
||
|
||
## Job Middleware
|
||
|
||
Job middleware allow you to wrap custom logic around the execution of queued
|
||
jobs, reducing boilerplate in the jobs themselves. For example, consider the
|
||
following `handle` method which leverages Laravel's Redis rate limiting
|
||
features to allow only one job to process every five seconds:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Redis;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Execute the job.
|
||
|
||
5 */
|
||
|
||
6public function handle(): void
|
||
|
||
7{
|
||
|
||
8 Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
|
||
|
||
9 info('Lock obtained...');
|
||
|
||
10
|
||
|
||
11 // Handle job...
|
||
|
||
12 }, function () {
|
||
|
||
13 // Could not obtain lock...
|
||
|
||
14
|
||
|
||
15 return $this->release(5);
|
||
|
||
16 });
|
||
|
||
17}
|
||
|
||
|
||
use Illuminate\Support\Facades\Redis;
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
|
||
info('Lock obtained...');
|
||
|
||
// Handle job...
|
||
}, function () {
|
||
// Could not obtain lock...
|
||
|
||
return $this->release(5);
|
||
});
|
||
}
|
||
|
||
While this code is valid, the implementation of the `handle` method becomes
|
||
noisy since it is cluttered with Redis rate limiting logic. In addition, this
|
||
rate limiting logic must be duplicated for any other jobs that we want to rate
|
||
limit. Instead of rate limiting in the handle method, we could define a job
|
||
middleware that handles rate limiting:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs\Middleware;
|
||
|
||
4
|
||
|
||
5use Closure;
|
||
|
||
6use Illuminate\Support\Facades\Redis;
|
||
|
||
7
|
||
|
||
8class RateLimited
|
||
|
||
9{
|
||
|
||
10 /**
|
||
|
||
11 * Process the queued job.
|
||
|
||
12 *
|
||
|
||
13 * @param \Closure(object): void $next
|
||
|
||
14 */
|
||
|
||
15 public function handle(object $job, Closure $next): void
|
||
|
||
16 {
|
||
|
||
17 Redis::throttle('key')
|
||
|
||
18 ->block(0)->allow(1)->every(5)
|
||
|
||
19 ->then(function () use ($job, $next) {
|
||
|
||
20 // Lock obtained...
|
||
|
||
21
|
||
|
||
22 $next($job);
|
||
|
||
23 }, function () use ($job) {
|
||
|
||
24 // Could not obtain lock...
|
||
|
||
25
|
||
|
||
26 $job->release(5);
|
||
|
||
27 });
|
||
|
||
28 }
|
||
|
||
29}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs\Middleware;
|
||
|
||
use Closure;
|
||
use Illuminate\Support\Facades\Redis;
|
||
|
||
class RateLimited
|
||
{
|
||
/**
|
||
* Process the queued job.
|
||
*
|
||
* @param \Closure(object): void $next
|
||
*/
|
||
public function handle(object $job, Closure $next): void
|
||
{
|
||
Redis::throttle('key')
|
||
->block(0)->allow(1)->every(5)
|
||
->then(function () use ($job, $next) {
|
||
// Lock obtained...
|
||
|
||
$next($job);
|
||
}, function () use ($job) {
|
||
// Could not obtain lock...
|
||
|
||
$job->release(5);
|
||
});
|
||
}
|
||
}
|
||
|
||
As you can see, like [route middleware](/docs/12.x/middleware), job middleware
|
||
receive the job being processed and a callback that should be invoked to
|
||
continue processing the job.
|
||
|
||
You can generate a new job middleware class using the `make:job-middleware`
|
||
Artisan command. After creating job middleware, they may be attached to a job
|
||
by returning them from the job's `middleware` method. This method does not
|
||
exist on jobs scaffolded by the `make:job` Artisan command, so you will need
|
||
to manually add it to your job class:
|
||
|
||
|
||
|
||
1use App\Jobs\Middleware\RateLimited;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 *
|
||
|
||
6 * @return array<int, object>
|
||
|
||
7 */
|
||
|
||
8public function middleware(): array
|
||
|
||
9{
|
||
|
||
10 return [new RateLimited];
|
||
|
||
11}
|
||
|
||
|
||
use App\Jobs\Middleware\RateLimited;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [new RateLimited];
|
||
}
|
||
|
||
Job middleware can also be assigned to [queueable event
|
||
listeners](/docs/12.x/events#queued-event-listeners),
|
||
[mailables](/docs/12.x/mail#queueing-mail), and
|
||
[notifications](/docs/12.x/notifications#queueing-notifications).
|
||
|
||
### Rate Limiting
|
||
|
||
Although we just demonstrated how to write your own rate limiting job
|
||
middleware, Laravel actually includes a rate limiting middleware that you may
|
||
utilize to rate limit jobs. Like [route rate
|
||
limiters](/docs/12.x/routing#defining-rate-limiters), job rate limiters are
|
||
defined using the `RateLimiter` facade's `for` method.
|
||
|
||
For example, you may wish to allow users to backup their data once per hour
|
||
while imposing no such limit on premium customers. To accomplish this, you may
|
||
define a `RateLimiter` in the `boot` method of your `AppServiceProvider`:
|
||
|
||
|
||
|
||
1use Illuminate\Cache\RateLimiting\Limit;
|
||
|
||
2use Illuminate\Support\Facades\RateLimiter;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Bootstrap any application services.
|
||
|
||
6 */
|
||
|
||
7public function boot(): void
|
||
|
||
8{
|
||
|
||
9 RateLimiter::for('backups', function (object $job) {
|
||
|
||
10 return $job->user->vipCustomer()
|
||
|
||
11 ? Limit::none()
|
||
|
||
12 : Limit::perHour(1)->by($job->user->id);
|
||
|
||
13 });
|
||
|
||
14}
|
||
|
||
|
||
use Illuminate\Cache\RateLimiting\Limit;
|
||
use Illuminate\Support\Facades\RateLimiter;
|
||
|
||
/**
|
||
* Bootstrap any application services.
|
||
*/
|
||
public function boot(): void
|
||
{
|
||
RateLimiter::for('backups', function (object $job) {
|
||
return $job->user->vipCustomer()
|
||
? Limit::none()
|
||
: Limit::perHour(1)->by($job->user->id);
|
||
});
|
||
}
|
||
|
||
In the example above, we defined an hourly rate limit; however, you may easily
|
||
define a rate limit based on minutes using the `perMinute` method. In
|
||
addition, you may pass any value you wish to the `by` method of the rate
|
||
limit; however, this value is most often used to segment rate limits by
|
||
customer:
|
||
|
||
|
||
|
||
1return Limit::perMinute(50)->by($job->user->id);
|
||
|
||
|
||
return Limit::perMinute(50)->by($job->user->id);
|
||
|
||
Once you have defined your rate limit, you may attach the rate limiter to your
|
||
job using the `Illuminate\Queue\Middleware\RateLimited` middleware. Each time
|
||
the job exceeds the rate limit, this middleware will release the job back to
|
||
the queue with an appropriate delay based on the rate limit duration:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\RateLimited;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 *
|
||
|
||
6 * @return array<int, object>
|
||
|
||
7 */
|
||
|
||
8public function middleware(): array
|
||
|
||
9{
|
||
|
||
10 return [new RateLimited('backups')];
|
||
|
||
11}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\RateLimited;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [new RateLimited('backups')];
|
||
}
|
||
|
||
Releasing a rate limited job back onto the queue will still increment the
|
||
job's total number of `attempts`. You may wish to tune your `tries` and
|
||
`maxExceptions` properties on your job class accordingly. Or, you may wish to
|
||
use the retryUntil method to define the amount of time until the job should no
|
||
longer be attempted.
|
||
|
||
Using the `releaseAfter` method, you may also specify the number of seconds
|
||
that must elapse before the released job will be attempted again:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Get the middleware the job should pass through.
|
||
|
||
3 *
|
||
|
||
4 * @return array<int, object>
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [(new RateLimited('backups'))->releaseAfter(60)];
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new RateLimited('backups'))->releaseAfter(60)];
|
||
}
|
||
|
||
If you do not want a job to be retried when it is rate limited, you may use
|
||
the `dontRelease` method:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Get the middleware the job should pass through.
|
||
|
||
3 *
|
||
|
||
4 * @return array<int, object>
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [(new RateLimited('backups'))->dontRelease()];
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new RateLimited('backups'))->dontRelease()];
|
||
}
|
||
|
||
If you are using Redis, you may use the
|
||
`Illuminate\Queue\Middleware\RateLimitedWithRedis` middleware, which is fine-
|
||
tuned for Redis and more efficient than the basic rate limiting middleware.
|
||
|
||
### Preventing Job Overlaps
|
||
|
||
Laravel includes an `Illuminate\Queue\Middleware\WithoutOverlapping`
|
||
middleware that allows you to prevent job overlaps based on an arbitrary key.
|
||
This can be helpful when a queued job is modifying a resource that should only
|
||
be modified by one job at a time.
|
||
|
||
For example, let's imagine you have a queued job that updates a user's credit
|
||
score and you want to prevent credit score update job overlaps for the same
|
||
user ID. To accomplish this, you can return the `WithoutOverlapping`
|
||
middleware from your job's `middleware` method:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 *
|
||
|
||
6 * @return array<int, object>
|
||
|
||
7 */
|
||
|
||
8public function middleware(): array
|
||
|
||
9{
|
||
|
||
10 return [new WithoutOverlapping($this->user->id)];
|
||
|
||
11}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [new WithoutOverlapping($this->user->id)];
|
||
}
|
||
|
||
Releasing an overlapping job back onto the queue will still increment the
|
||
job's total number of attempts. You may wish to tune your `tries` and
|
||
`maxExceptions` properties on your job class accordingly. For example, leaving
|
||
the `tries` property to 1 as it is by default would prevent any overlapping
|
||
job from being retried later.
|
||
|
||
Any overlapping jobs of the same type will be released back to the queue. You
|
||
may also specify the number of seconds that must elapse before the released
|
||
job will be attempted again:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Get the middleware the job should pass through.
|
||
|
||
3 *
|
||
|
||
4 * @return array<int, object>
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
|
||
}
|
||
|
||
If you wish to immediately delete any overlapping jobs so that they will not
|
||
be retried, you may use the `dontRelease` method:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Get the middleware the job should pass through.
|
||
|
||
3 *
|
||
|
||
4 * @return array<int, object>
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [(new WithoutOverlapping($this->order->id))->dontRelease()];
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new WithoutOverlapping($this->order->id))->dontRelease()];
|
||
}
|
||
|
||
The `WithoutOverlapping` middleware is powered by Laravel's atomic lock
|
||
feature. Sometimes, your job may unexpectedly fail or timeout in such a way
|
||
that the lock is not released. Therefore, you may explicitly define a lock
|
||
expiration time using the `expireAfter` method. For example, the example below
|
||
will instruct Laravel to release the `WithoutOverlapping` lock three minutes
|
||
after the job has started processing:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Get the middleware the job should pass through.
|
||
|
||
3 *
|
||
|
||
4 * @return array<int, object>
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
|
||
}
|
||
|
||
The `WithoutOverlapping` middleware requires a cache driver that supports
|
||
[locks](/docs/12.x/cache#atomic-locks). Currently, the `memcached`, `redis`,
|
||
`dynamodb`, `database`, `file`, and `array` cache drivers support atomic
|
||
locks.
|
||
|
||
#### Sharing Lock Keys Across Job Classes
|
||
|
||
By default, the `WithoutOverlapping` middleware will only prevent overlapping
|
||
jobs of the same class. So, although two different job classes may use the
|
||
same lock key, they will not be prevented from overlapping. However, you can
|
||
instruct Laravel to apply the key across job classes using the `shared`
|
||
method:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||
|
||
2
|
||
|
||
3class ProviderIsDown
|
||
|
||
4{
|
||
|
||
5 // ...
|
||
|
||
6
|
||
|
||
7 public function middleware(): array
|
||
|
||
8 {
|
||
|
||
9 return [
|
||
|
||
10 (new WithoutOverlapping("status:{$this->provider}"))->shared(),
|
||
|
||
11 ];
|
||
|
||
12 }
|
||
|
||
13}
|
||
|
||
14
|
||
|
||
15class ProviderIsUp
|
||
|
||
16{
|
||
|
||
17 // ...
|
||
|
||
18
|
||
|
||
19 public function middleware(): array
|
||
|
||
20 {
|
||
|
||
21 return [
|
||
|
||
22 (new WithoutOverlapping("status:{$this->provider}"))->shared(),
|
||
|
||
23 ];
|
||
|
||
24 }
|
||
|
||
25}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||
|
||
class ProviderIsDown
|
||
{
|
||
// ...
|
||
|
||
public function middleware(): array
|
||
{
|
||
return [
|
||
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
|
||
];
|
||
}
|
||
}
|
||
|
||
class ProviderIsUp
|
||
{
|
||
// ...
|
||
|
||
public function middleware(): array
|
||
{
|
||
return [
|
||
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
|
||
];
|
||
}
|
||
}
|
||
|
||
### Throttling Exceptions
|
||
|
||
Laravel includes a `Illuminate\Queue\Middleware\ThrottlesExceptions`
|
||
middleware that allows you to throttle exceptions. Once the job throws a given
|
||
number of exceptions, all further attempts to execute the job are delayed
|
||
until a specified time interval lapses. This middleware is particularly useful
|
||
for jobs that interact with third-party services that are unstable.
|
||
|
||
For example, let's imagine a queued job that interacts with a third-party API
|
||
that begins throwing exceptions. To throttle exceptions, you can return the
|
||
`ThrottlesExceptions` middleware from your job's `middleware` method.
|
||
Typically, this middleware should be paired with a job that implements time
|
||
based attempts:
|
||
|
||
|
||
|
||
1use DateTime;
|
||
|
||
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Get the middleware the job should pass through.
|
||
|
||
6 *
|
||
|
||
7 * @return array<int, object>
|
||
|
||
8 */
|
||
|
||
9public function middleware(): array
|
||
|
||
10{
|
||
|
||
11 return [new ThrottlesExceptions(10, 5 * 60)];
|
||
|
||
12}
|
||
|
||
13
|
||
|
||
14/**
|
||
|
||
15 * Determine the time at which the job should timeout.
|
||
|
||
16 */
|
||
|
||
17public function retryUntil(): DateTime
|
||
|
||
18{
|
||
|
||
19 return now()->addMinutes(30);
|
||
|
||
20}
|
||
|
||
|
||
use DateTime;
|
||
use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [new ThrottlesExceptions(10, 5 * 60)];
|
||
}
|
||
|
||
/**
|
||
* Determine the time at which the job should timeout.
|
||
*/
|
||
public function retryUntil(): DateTime
|
||
{
|
||
return now()->addMinutes(30);
|
||
}
|
||
|
||
The first constructor argument accepted by the middleware is the number of
|
||
exceptions the job can throw before being throttled, while the second
|
||
constructor argument is the number of seconds that should elapse before the
|
||
job is attempted again once it has been throttled. In the code example above,
|
||
if the job throws 10 consecutive exceptions, we will wait 5 minutes before
|
||
attempting the job again, constrained by the 30-minute time limit.
|
||
|
||
When a job throws an exception but the exception threshold has not yet been
|
||
reached, the job will typically be retried immediately. However, you may
|
||
specify the number of minutes such a job should be delayed by calling the
|
||
`backoff` method when attaching the middleware to the job:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 *
|
||
|
||
6 * @return array<int, object>
|
||
|
||
7 */
|
||
|
||
8public function middleware(): array
|
||
|
||
9{
|
||
|
||
10 return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
|
||
|
||
11}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
|
||
}
|
||
|
||
Internally, this middleware uses Laravel's cache system to implement rate
|
||
limiting, and the job's class name is utilized as the cache "key". You may
|
||
override this key by calling the `by` method when attaching the middleware to
|
||
your job. This may be useful if you have multiple jobs interacting with the
|
||
same third-party service and you would like them to share a common throttling
|
||
"bucket" ensuring they respect a single shared limit:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 *
|
||
|
||
6 * @return array<int, object>
|
||
|
||
7 */
|
||
|
||
8public function middleware(): array
|
||
|
||
9{
|
||
|
||
10 return [(new ThrottlesExceptions(10, 10 * 60))->by('key')];
|
||
|
||
11}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new ThrottlesExceptions(10, 10 * 60))->by('key')];
|
||
}
|
||
|
||
By default, this middleware will throttle every exception. You can modify this
|
||
behavior by invoking the `when` method when attaching the middleware to your
|
||
job. The exception will then only be throttled if the closure provided to the
|
||
`when` method returns `true`:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Client\HttpClientException;
|
||
|
||
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Get the middleware the job should pass through.
|
||
|
||
6 *
|
||
|
||
7 * @return array<int, object>
|
||
|
||
8 */
|
||
|
||
9public function middleware(): array
|
||
|
||
10{
|
||
|
||
11 return [(new ThrottlesExceptions(10, 10 * 60))->when(
|
||
|
||
12 fn (Throwable $throwable) => $throwable instanceof HttpClientException
|
||
|
||
13 )];
|
||
|
||
14}
|
||
|
||
|
||
use Illuminate\Http\Client\HttpClientException;
|
||
use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new ThrottlesExceptions(10, 10 * 60))->when(
|
||
fn (Throwable $throwable) => $throwable instanceof HttpClientException
|
||
)];
|
||
}
|
||
|
||
Unlike the `when` method, which releases the job back onto the queue or throws
|
||
an exception, the `deleteWhen` method allows you to delete the job entirely
|
||
when a given exception occurs:
|
||
|
||
|
||
|
||
1use App\Exceptions\CustomerDeletedException;
|
||
|
||
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Get the middleware the job should pass through.
|
||
|
||
6 *
|
||
|
||
7 * @return array<int, object>
|
||
|
||
8 */
|
||
|
||
9public function middleware(): array
|
||
|
||
10{
|
||
|
||
11 return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(CustomerDeletedException::class)];
|
||
|
||
12}
|
||
|
||
|
||
use App\Exceptions\CustomerDeletedException;
|
||
use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(CustomerDeletedException::class)];
|
||
}
|
||
|
||
If you would like to have the throttled exceptions reported to your
|
||
application's exception handler, you can do so by invoking the `report` method
|
||
when attaching the middleware to your job. Optionally, you may provide a
|
||
closure to the `report` method and the exception will only be reported if the
|
||
given closure returns `true`:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Client\HttpClientException;
|
||
|
||
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Get the middleware the job should pass through.
|
||
|
||
6 *
|
||
|
||
7 * @return array<int, object>
|
||
|
||
8 */
|
||
|
||
9public function middleware(): array
|
||
|
||
10{
|
||
|
||
11 return [(new ThrottlesExceptions(10, 10 * 60))->report(
|
||
|
||
12 fn (Throwable $throwable) => $throwable instanceof HttpClientException
|
||
|
||
13 )];
|
||
|
||
14}
|
||
|
||
|
||
use Illuminate\Http\Client\HttpClientException;
|
||
use Illuminate\Queue\Middleware\ThrottlesExceptions;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*
|
||
* @return array<int, object>
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [(new ThrottlesExceptions(10, 10 * 60))->report(
|
||
fn (Throwable $throwable) => $throwable instanceof HttpClientException
|
||
)];
|
||
}
|
||
|
||
If you are using Redis, you may use the
|
||
`Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis` middleware, which
|
||
is fine-tuned for Redis and more efficient than the basic exception throttling
|
||
middleware.
|
||
|
||
### Skipping Jobs
|
||
|
||
The `Skip` middleware allows you to specify that a job should be skipped /
|
||
deleted without needing to modify the job's logic. The `Skip::when` method
|
||
will delete the job if the given condition evaluates to `true`, while the
|
||
`Skip::unless` method will delete the job if the condition evaluates to
|
||
`false`:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\Skip;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [
|
||
|
||
9 Skip::when($condition),
|
||
|
||
10 ];
|
||
|
||
11}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\Skip;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [
|
||
Skip::when($condition),
|
||
];
|
||
}
|
||
|
||
You can also pass a `Closure` to the `when` and `unless` methods for more
|
||
complex conditional evaluation:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\Skip;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [
|
||
|
||
9 Skip::when(function (): bool {
|
||
|
||
10 return $this->shouldSkip();
|
||
|
||
11 }),
|
||
|
||
12 ];
|
||
|
||
13}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\Skip;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [
|
||
Skip::when(function (): bool {
|
||
return $this->shouldSkip();
|
||
}),
|
||
];
|
||
}
|
||
|
||
## Dispatching Jobs
|
||
|
||
Once you have written your job class, you may dispatch it using the `dispatch`
|
||
method on the job itself. The arguments passed to the `dispatch` method will
|
||
be given to the job's constructor:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Controllers;
|
||
|
||
4
|
||
|
||
5use App\Jobs\ProcessPodcast;
|
||
|
||
6use App\Models\Podcast;
|
||
|
||
7use Illuminate\Http\RedirectResponse;
|
||
|
||
8use Illuminate\Http\Request;
|
||
|
||
9
|
||
|
||
10class PodcastController extends Controller
|
||
|
||
11{
|
||
|
||
12 /**
|
||
|
||
13 * Store a new podcast.
|
||
|
||
14 */
|
||
|
||
15 public function store(Request $request): RedirectResponse
|
||
|
||
16 {
|
||
|
||
17 $podcast = Podcast::create(/* ... */);
|
||
|
||
18
|
||
|
||
19 // ...
|
||
|
||
20
|
||
|
||
21 ProcessPodcast::dispatch($podcast);
|
||
|
||
22
|
||
|
||
23 return redirect('/podcasts');
|
||
|
||
24 }
|
||
|
||
25}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Models\Podcast;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
|
||
class PodcastController extends Controller
|
||
{
|
||
/**
|
||
* Store a new podcast.
|
||
*/
|
||
public function store(Request $request): RedirectResponse
|
||
{
|
||
$podcast = Podcast::create(/* ... */);
|
||
|
||
// ...
|
||
|
||
ProcessPodcast::dispatch($podcast);
|
||
|
||
return redirect('/podcasts');
|
||
}
|
||
}
|
||
|
||
If you would like to conditionally dispatch a job, you may use the
|
||
`dispatchIf` and `dispatchUnless` methods:
|
||
|
||
|
||
|
||
1ProcessPodcast::dispatchIf($accountActive, $podcast);
|
||
|
||
2
|
||
|
||
3ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
|
||
|
||
|
||
ProcessPodcast::dispatchIf($accountActive, $podcast);
|
||
|
||
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
|
||
|
||
In new Laravel applications, the `database` driver is the default queue
|
||
driver. You may specify a different queue driver within your application's
|
||
`config/queue.php` configuration file.
|
||
|
||
### Delayed Dispatching
|
||
|
||
If you would like to specify that a job should not be immediately available
|
||
for processing by a queue worker, you may use the `delay` method when
|
||
dispatching the job. For example, let's specify that a job should not be
|
||
available for processing until 10 minutes after it has been dispatched:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Controllers;
|
||
|
||
4
|
||
|
||
5use App\Jobs\ProcessPodcast;
|
||
|
||
6use App\Models\Podcast;
|
||
|
||
7use Illuminate\Http\RedirectResponse;
|
||
|
||
8use Illuminate\Http\Request;
|
||
|
||
9
|
||
|
||
10class PodcastController extends Controller
|
||
|
||
11{
|
||
|
||
12 /**
|
||
|
||
13 * Store a new podcast.
|
||
|
||
14 */
|
||
|
||
15 public function store(Request $request): RedirectResponse
|
||
|
||
16 {
|
||
|
||
17 $podcast = Podcast::create(/* ... */);
|
||
|
||
18
|
||
|
||
19 // ...
|
||
|
||
20
|
||
|
||
21 ProcessPodcast::dispatch($podcast)
|
||
|
||
22 ->delay(now()->addMinutes(10));
|
||
|
||
23
|
||
|
||
24 return redirect('/podcasts');
|
||
|
||
25 }
|
||
|
||
26}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Models\Podcast;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
|
||
class PodcastController extends Controller
|
||
{
|
||
/**
|
||
* Store a new podcast.
|
||
*/
|
||
public function store(Request $request): RedirectResponse
|
||
{
|
||
$podcast = Podcast::create(/* ... */);
|
||
|
||
// ...
|
||
|
||
ProcessPodcast::dispatch($podcast)
|
||
->delay(now()->addMinutes(10));
|
||
|
||
return redirect('/podcasts');
|
||
}
|
||
}
|
||
|
||
In some cases, jobs may have a default delay configured. If you need to bypass
|
||
this delay and dispatch a job for immediate processing, you may use the
|
||
`withoutDelay` method:
|
||
|
||
|
||
|
||
1ProcessPodcast::dispatch($podcast)->withoutDelay();
|
||
|
||
|
||
ProcessPodcast::dispatch($podcast)->withoutDelay();
|
||
|
||
The Amazon SQS queue service has a maximum delay time of 15 minutes.
|
||
|
||
#### Dispatching After the Response is Sent to the Browser
|
||
|
||
Alternatively, the `dispatchAfterResponse` method delays dispatching a job
|
||
until after the HTTP response is sent to the user's browser if your web server
|
||
is using [FastCGI](https://www.php.net/manual/en/install.fpm.php). This will
|
||
still allow the user to begin using the application even though a queued job
|
||
is still executing. This should typically only be used for jobs that take
|
||
about a second, such as sending an email. Since they are processed within the
|
||
current HTTP request, jobs dispatched in this fashion do not require a queue
|
||
worker to be running in order for them to be processed:
|
||
|
||
|
||
|
||
1use App\Jobs\SendNotification;
|
||
|
||
2
|
||
|
||
3SendNotification::dispatchAfterResponse();
|
||
|
||
|
||
use App\Jobs\SendNotification;
|
||
|
||
SendNotification::dispatchAfterResponse();
|
||
|
||
You may also `dispatch` a closure and chain the `afterResponse` method onto
|
||
the [dispatch helper](/docs/12.x/helpers#method-dispatch) to execute a closure
|
||
after the HTTP response has been sent to the browser:
|
||
|
||
|
||
|
||
1use App\Mail\WelcomeMessage;
|
||
|
||
2use Illuminate\Support\Facades\Mail;
|
||
|
||
3
|
||
|
||
4dispatch(function () {
|
||
|
||
5 Mail::to('[[email protected]](/cdn-cgi/l/email-protection)')->send(new WelcomeMessage);
|
||
|
||
6})->afterResponse();
|
||
|
||
|
||
use App\Mail\WelcomeMessage;
|
||
use Illuminate\Support\Facades\Mail;
|
||
|
||
dispatch(function () {
|
||
Mail::to('[[email protected]](/cdn-cgi/l/email-protection)')->send(new WelcomeMessage);
|
||
})->afterResponse();
|
||
|
||
### Synchronous Dispatching
|
||
|
||
If you would like to dispatch a job immediately (synchronously), you may use
|
||
the `dispatchSync` method. When using this method, the job will not be queued
|
||
and will be executed immediately within the current process:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Controllers;
|
||
|
||
4
|
||
|
||
5use App\Jobs\ProcessPodcast;
|
||
|
||
6use App\Models\Podcast;
|
||
|
||
7use Illuminate\Http\RedirectResponse;
|
||
|
||
8use Illuminate\Http\Request;
|
||
|
||
9
|
||
|
||
10class PodcastController extends Controller
|
||
|
||
11{
|
||
|
||
12 /**
|
||
|
||
13 * Store a new podcast.
|
||
|
||
14 */
|
||
|
||
15 public function store(Request $request): RedirectResponse
|
||
|
||
16 {
|
||
|
||
17 $podcast = Podcast::create(/* ... */);
|
||
|
||
18
|
||
|
||
19 // Create podcast...
|
||
|
||
20
|
||
|
||
21 ProcessPodcast::dispatchSync($podcast);
|
||
|
||
22
|
||
|
||
23 return redirect('/podcasts');
|
||
|
||
24 }
|
||
|
||
25}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Models\Podcast;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
|
||
class PodcastController extends Controller
|
||
{
|
||
/**
|
||
* Store a new podcast.
|
||
*/
|
||
public function store(Request $request): RedirectResponse
|
||
{
|
||
$podcast = Podcast::create(/* ... */);
|
||
|
||
// Create podcast...
|
||
|
||
ProcessPodcast::dispatchSync($podcast);
|
||
|
||
return redirect('/podcasts');
|
||
}
|
||
}
|
||
|
||
### Jobs & Database Transactions
|
||
|
||
While it is perfectly fine to dispatch jobs within database transactions, you
|
||
should take special care to ensure that your job will actually be able to
|
||
execute successfully. When dispatching a job within a transaction, it is
|
||
possible that the job will be processed by a worker before the parent
|
||
transaction has committed. When this happens, any updates you have made to
|
||
models or database records during the database transaction(s) may not yet be
|
||
reflected in the database. In addition, any models or database records created
|
||
within the transaction(s) may not exist in the database.
|
||
|
||
Thankfully, Laravel provides several methods of working around this problem.
|
||
First, you may set the `after_commit` connection option in your queue
|
||
connection's configuration array:
|
||
|
||
|
||
|
||
1'redis' => [
|
||
|
||
2 'driver' => 'redis',
|
||
|
||
3 // ...
|
||
|
||
4 'after_commit' => true,
|
||
|
||
5],
|
||
|
||
|
||
'redis' => [
|
||
'driver' => 'redis',
|
||
// ...
|
||
'after_commit' => true,
|
||
],
|
||
|
||
When the `after_commit` option is `true`, you may dispatch jobs within
|
||
database transactions; however, Laravel will wait until the open parent
|
||
database transactions have been committed before actually dispatching the job.
|
||
Of course, if no database transactions are currently open, the job will be
|
||
dispatched immediately.
|
||
|
||
If a transaction is rolled back due to an exception that occurs during the
|
||
transaction, the jobs that were dispatched during that transaction will be
|
||
discarded.
|
||
|
||
Setting the `after_commit` configuration option to `true` will also cause any
|
||
queued event listeners, mailables, notifications, and broadcast events to be
|
||
dispatched after all open database transactions have been committed.
|
||
|
||
#### Specifying Commit Dispatch Behavior Inline
|
||
|
||
If you do not set the `after_commit` queue connection configuration option to
|
||
`true`, you may still indicate that a specific job should be dispatched after
|
||
all open database transactions have been committed. To accomplish this, you
|
||
may chain the `afterCommit` method onto your dispatch operation:
|
||
|
||
|
||
|
||
1use App\Jobs\ProcessPodcast;
|
||
|
||
2
|
||
|
||
3ProcessPodcast::dispatch($podcast)->afterCommit();
|
||
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
|
||
ProcessPodcast::dispatch($podcast)->afterCommit();
|
||
|
||
Likewise, if the `after_commit` configuration option is set to `true`, you may
|
||
indicate that a specific job should be dispatched immediately without waiting
|
||
for any open database transactions to commit:
|
||
|
||
|
||
|
||
1ProcessPodcast::dispatch($podcast)->beforeCommit();
|
||
|
||
|
||
ProcessPodcast::dispatch($podcast)->beforeCommit();
|
||
|
||
### Job Chaining
|
||
|
||
Job chaining allows you to specify a list of queued jobs that should be run in
|
||
sequence after the primary job has executed successfully. If one job in the
|
||
sequence fails, the rest of the jobs will not be run. To execute a queued job
|
||
chain, you may use the `chain` method provided by the `Bus` facade. Laravel's
|
||
command bus is a lower-level component that queued job dispatching is built on
|
||
top of:
|
||
|
||
|
||
|
||
1use App\Jobs\OptimizePodcast;
|
||
|
||
2use App\Jobs\ProcessPodcast;
|
||
|
||
3use App\Jobs\ReleasePodcast;
|
||
|
||
4use Illuminate\Support\Facades\Bus;
|
||
|
||
5
|
||
|
||
6Bus::chain([
|
||
|
||
7 new ProcessPodcast,
|
||
|
||
8 new OptimizePodcast,
|
||
|
||
9 new ReleasePodcast,
|
||
|
||
10])->dispatch();
|
||
|
||
|
||
use App\Jobs\OptimizePodcast;
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Jobs\ReleasePodcast;
|
||
use Illuminate\Support\Facades\Bus;
|
||
|
||
Bus::chain([
|
||
new ProcessPodcast,
|
||
new OptimizePodcast,
|
||
new ReleasePodcast,
|
||
])->dispatch();
|
||
|
||
In addition to chaining job class instances, you may also chain closures:
|
||
|
||
|
||
|
||
1Bus::chain([
|
||
|
||
2 new ProcessPodcast,
|
||
|
||
3 new OptimizePodcast,
|
||
|
||
4 function () {
|
||
|
||
5 Podcast::update(/* ... */);
|
||
|
||
6 },
|
||
|
||
7])->dispatch();
|
||
|
||
|
||
Bus::chain([
|
||
new ProcessPodcast,
|
||
new OptimizePodcast,
|
||
function () {
|
||
Podcast::update(/* ... */);
|
||
},
|
||
])->dispatch();
|
||
|
||
Deleting jobs using the `$this->delete()` method within the job will not
|
||
prevent chained jobs from being processed. The chain will only stop executing
|
||
if a job in the chain fails.
|
||
|
||
#### Chain Connection and Queue
|
||
|
||
If you would like to specify the connection and queue that should be used for
|
||
the chained jobs, you may use the `onConnection` and `onQueue` methods. These
|
||
methods specify the queue connection and queue name that should be used unless
|
||
the queued job is explicitly assigned a different connection / queue:
|
||
|
||
|
||
|
||
1Bus::chain([
|
||
|
||
2 new ProcessPodcast,
|
||
|
||
3 new OptimizePodcast,
|
||
|
||
4 new ReleasePodcast,
|
||
|
||
5])->onConnection('redis')->onQueue('podcasts')->dispatch();
|
||
|
||
|
||
Bus::chain([
|
||
new ProcessPodcast,
|
||
new OptimizePodcast,
|
||
new ReleasePodcast,
|
||
])->onConnection('redis')->onQueue('podcasts')->dispatch();
|
||
|
||
#### Adding Jobs to the Chain
|
||
|
||
Occasionally, you may need to prepend or append a job to an existing job chain
|
||
from within another job in that chain. You may accomplish this using the
|
||
`prependToChain` and `appendToChain` methods:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Execute the job.
|
||
|
||
3 */
|
||
|
||
4public function handle(): void
|
||
|
||
5{
|
||
|
||
6 // ...
|
||
|
||
7
|
||
|
||
8 // Prepend to the current chain, run job immediately after current job...
|
||
|
||
9 $this->prependToChain(new TranscribePodcast);
|
||
|
||
10
|
||
|
||
11 // Append to the current chain, run job at end of chain...
|
||
|
||
12 $this->appendToChain(new TranscribePodcast);
|
||
|
||
13}
|
||
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
// ...
|
||
|
||
// Prepend to the current chain, run job immediately after current job...
|
||
$this->prependToChain(new TranscribePodcast);
|
||
|
||
// Append to the current chain, run job at end of chain...
|
||
$this->appendToChain(new TranscribePodcast);
|
||
}
|
||
|
||
#### Chain Failures
|
||
|
||
When chaining jobs, you may use the `catch` method to specify a closure that
|
||
should be invoked if a job within the chain fails. The given callback will
|
||
receive the `Throwable` instance that caused the job failure:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Bus;
|
||
|
||
2use Throwable;
|
||
|
||
3
|
||
|
||
4Bus::chain([
|
||
|
||
5 new ProcessPodcast,
|
||
|
||
6 new OptimizePodcast,
|
||
|
||
7 new ReleasePodcast,
|
||
|
||
8])->catch(function (Throwable $e) {
|
||
|
||
9 // A job within the chain has failed...
|
||
|
||
10})->dispatch();
|
||
|
||
|
||
use Illuminate\Support\Facades\Bus;
|
||
use Throwable;
|
||
|
||
Bus::chain([
|
||
new ProcessPodcast,
|
||
new OptimizePodcast,
|
||
new ReleasePodcast,
|
||
])->catch(function (Throwable $e) {
|
||
// A job within the chain has failed...
|
||
})->dispatch();
|
||
|
||
Since chain callbacks are serialized and executed at a later time by the
|
||
Laravel queue, you should not use the `$this` variable within chain callbacks.
|
||
|
||
### Customizing the Queue and Connection
|
||
|
||
#### Dispatching to a Particular Queue
|
||
|
||
By pushing jobs to different queues, you may "categorize" your queued jobs and
|
||
even prioritize how many workers you assign to various queues. Keep in mind,
|
||
this does not push jobs to different queue "connections" as defined by your
|
||
queue configuration file, but only to specific queues within a single
|
||
connection. To specify the queue, use the `onQueue` method when dispatching
|
||
the job:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Controllers;
|
||
|
||
4
|
||
|
||
5use App\Jobs\ProcessPodcast;
|
||
|
||
6use App\Models\Podcast;
|
||
|
||
7use Illuminate\Http\RedirectResponse;
|
||
|
||
8use Illuminate\Http\Request;
|
||
|
||
9
|
||
|
||
10class PodcastController extends Controller
|
||
|
||
11{
|
||
|
||
12 /**
|
||
|
||
13 * Store a new podcast.
|
||
|
||
14 */
|
||
|
||
15 public function store(Request $request): RedirectResponse
|
||
|
||
16 {
|
||
|
||
17 $podcast = Podcast::create(/* ... */);
|
||
|
||
18
|
||
|
||
19 // Create podcast...
|
||
|
||
20
|
||
|
||
21 ProcessPodcast::dispatch($podcast)->onQueue('processing');
|
||
|
||
22
|
||
|
||
23 return redirect('/podcasts');
|
||
|
||
24 }
|
||
|
||
25}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Models\Podcast;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
|
||
class PodcastController extends Controller
|
||
{
|
||
/**
|
||
* Store a new podcast.
|
||
*/
|
||
public function store(Request $request): RedirectResponse
|
||
{
|
||
$podcast = Podcast::create(/* ... */);
|
||
|
||
// Create podcast...
|
||
|
||
ProcessPodcast::dispatch($podcast)->onQueue('processing');
|
||
|
||
return redirect('/podcasts');
|
||
}
|
||
}
|
||
|
||
Alternatively, you may specify the job's queue by calling the `onQueue` method
|
||
within the job's constructor:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
6use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
7
|
||
|
||
8class ProcessPodcast implements ShouldQueue
|
||
|
||
9{
|
||
|
||
10 use Queueable;
|
||
|
||
11
|
||
|
||
12 /**
|
||
|
||
13 * Create a new job instance.
|
||
|
||
14 */
|
||
|
||
15 public function __construct()
|
||
|
||
16 {
|
||
|
||
17 $this->onQueue('processing');
|
||
|
||
18 }
|
||
|
||
19}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct()
|
||
{
|
||
$this->onQueue('processing');
|
||
}
|
||
}
|
||
|
||
#### Dispatching to a Particular Connection
|
||
|
||
If your application interacts with multiple queue connections, you may specify
|
||
which connection to push a job to using the `onConnection` method:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Controllers;
|
||
|
||
4
|
||
|
||
5use App\Jobs\ProcessPodcast;
|
||
|
||
6use App\Models\Podcast;
|
||
|
||
7use Illuminate\Http\RedirectResponse;
|
||
|
||
8use Illuminate\Http\Request;
|
||
|
||
9
|
||
|
||
10class PodcastController extends Controller
|
||
|
||
11{
|
||
|
||
12 /**
|
||
|
||
13 * Store a new podcast.
|
||
|
||
14 */
|
||
|
||
15 public function store(Request $request): RedirectResponse
|
||
|
||
16 {
|
||
|
||
17 $podcast = Podcast::create(/* ... */);
|
||
|
||
18
|
||
|
||
19 // Create podcast...
|
||
|
||
20
|
||
|
||
21 ProcessPodcast::dispatch($podcast)->onConnection('sqs');
|
||
|
||
22
|
||
|
||
23 return redirect('/podcasts');
|
||
|
||
24 }
|
||
|
||
25}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Jobs\ProcessPodcast;
|
||
use App\Models\Podcast;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
|
||
class PodcastController extends Controller
|
||
{
|
||
/**
|
||
* Store a new podcast.
|
||
*/
|
||
public function store(Request $request): RedirectResponse
|
||
{
|
||
$podcast = Podcast::create(/* ... */);
|
||
|
||
// Create podcast...
|
||
|
||
ProcessPodcast::dispatch($podcast)->onConnection('sqs');
|
||
|
||
return redirect('/podcasts');
|
||
}
|
||
}
|
||
|
||
You may chain the `onConnection` and `onQueue` methods together to specify the
|
||
connection and the queue for a job:
|
||
|
||
|
||
|
||
1ProcessPodcast::dispatch($podcast)
|
||
|
||
2 ->onConnection('sqs')
|
||
|
||
3 ->onQueue('processing');
|
||
|
||
|
||
ProcessPodcast::dispatch($podcast)
|
||
->onConnection('sqs')
|
||
->onQueue('processing');
|
||
|
||
Alternatively, you may specify the job's connection by calling the
|
||
`onConnection` method within the job's constructor:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
6use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
7
|
||
|
||
8class ProcessPodcast implements ShouldQueue
|
||
|
||
9{
|
||
|
||
10 use Queueable;
|
||
|
||
11
|
||
|
||
12 /**
|
||
|
||
13 * Create a new job instance.
|
||
|
||
14 */
|
||
|
||
15 public function __construct()
|
||
|
||
16 {
|
||
|
||
17 $this->onConnection('sqs');
|
||
|
||
18 }
|
||
|
||
19}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct()
|
||
{
|
||
$this->onConnection('sqs');
|
||
}
|
||
}
|
||
|
||
### Specifying Max Job Attempts / Timeout Values
|
||
|
||
#### Max Attempts
|
||
|
||
Job attempts are a core concept of Laravel's queue system and power many
|
||
advanced features. While they may seem confusing at first, it's important to
|
||
understand how they work before modifying the default configuration.
|
||
|
||
When a job is dispatched, it is pushed onto the queue. A worker then picks it
|
||
up and attempts to execute it. This is a job attempt.
|
||
|
||
However, an attempt does not necessarily mean the job's `handle` method was
|
||
executed. Attempts can also be "consumed" in several ways:
|
||
|
||
* The job encounters an unhandled exception during execution.
|
||
* The job is manually released back to the queue using `$this->release()`.
|
||
* Middleware such as `WithoutOverlapping` or `RateLimited` fails to acquire a lock and releases the job.
|
||
* The job timed out.
|
||
* The job's `handle` method runs and completes without throwing an exception.
|
||
|
||
You likely do not want to keep attempting a job indefinitely. Therefore,
|
||
Laravel provides various ways to specify how many times or for how long a job
|
||
may be attempted.
|
||
|
||
By default, Laravel will only attempt a job once. If your job uses middleware
|
||
like `WithoutOverlapping` or `RateLimited`, or if you're manually releasing
|
||
jobs, you will likely need to increase the number of allowed attempts via the
|
||
`tries` option.
|
||
|
||
One approach to specifying the maximum number of times a job may be attempted
|
||
is via the `--tries` switch on the Artisan command line. This will apply to
|
||
all jobs processed by the worker unless the job being processed specifies the
|
||
number of times it may be attempted:
|
||
|
||
|
||
|
||
1php artisan queue:work --tries=3
|
||
|
||
|
||
php artisan queue:work --tries=3
|
||
|
||
If a job exceeds its maximum number of attempts, it will be considered a
|
||
"failed" job. For more information on handling failed jobs, consult the failed
|
||
job documentation. If `--tries=0` is provided to the `queue:work` command, the
|
||
job will be retried indefinitely.
|
||
|
||
You may take a more granular approach by defining the maximum number of times
|
||
a job may be attempted on the job class itself. If the maximum number of
|
||
attempts is specified on the job, it will take precedence over the `--tries`
|
||
value provided on the command line:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5class ProcessPodcast implements ShouldQueue
|
||
|
||
6{
|
||
|
||
7 /**
|
||
|
||
8 * The number of times the job may be attempted.
|
||
|
||
9 *
|
||
|
||
10 * @var int
|
||
|
||
11 */
|
||
|
||
12 public $tries = 5;
|
||
|
||
13}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
/**
|
||
* The number of times the job may be attempted.
|
||
*
|
||
* @var int
|
||
*/
|
||
public $tries = 5;
|
||
}
|
||
|
||
If you need dynamic control over a particular job's maximum attempts, you may
|
||
define a `tries` method on the job:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Determine number of times the job may be attempted.
|
||
|
||
3 */
|
||
|
||
4public function tries(): int
|
||
|
||
5{
|
||
|
||
6 return 5;
|
||
|
||
7}
|
||
|
||
|
||
/**
|
||
* Determine number of times the job may be attempted.
|
||
*/
|
||
public function tries(): int
|
||
{
|
||
return 5;
|
||
}
|
||
|
||
#### Time Based Attempts
|
||
|
||
As an alternative to defining how many times a job may be attempted before it
|
||
fails, you may define a time at which the job should no longer be attempted.
|
||
This allows a job to be attempted any number of times within a given time
|
||
frame. To define the time at which a job should no longer be attempted, add a
|
||
`retryUntil` method to your job class. This method should return a `DateTime`
|
||
instance:
|
||
|
||
|
||
|
||
1use DateTime;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Determine the time at which the job should timeout.
|
||
|
||
5 */
|
||
|
||
6public function retryUntil(): DateTime
|
||
|
||
7{
|
||
|
||
8 return now()->addMinutes(10);
|
||
|
||
9}
|
||
|
||
|
||
use DateTime;
|
||
|
||
/**
|
||
* Determine the time at which the job should timeout.
|
||
*/
|
||
public function retryUntil(): DateTime
|
||
{
|
||
return now()->addMinutes(10);
|
||
}
|
||
|
||
If both `retryUntil` and `tries` are defined, Laravel gives precedence to the
|
||
`retryUntil` method.
|
||
|
||
You may also define a `tries` property or `retryUntil` method on your [queued
|
||
event listeners](/docs/12.x/events#queued-event-listeners) and [queued
|
||
notifications](/docs/12.x/notifications#queueing-notifications).
|
||
|
||
#### Max Exceptions
|
||
|
||
Sometimes you may wish to specify that a job may be attempted many times, but
|
||
should fail if the retries are triggered by a given number of unhandled
|
||
exceptions (as opposed to being released by the `release` method directly). To
|
||
accomplish this, you may define a `maxExceptions` property on your job class:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
6use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
7use Illuminate\Support\Facades\Redis;
|
||
|
||
8
|
||
|
||
9class ProcessPodcast implements ShouldQueue
|
||
|
||
10{
|
||
|
||
11 use Queueable;
|
||
|
||
12
|
||
|
||
13 /**
|
||
|
||
14 * The number of times the job may be attempted.
|
||
|
||
15 *
|
||
|
||
16 * @var int
|
||
|
||
17 */
|
||
|
||
18 public $tries = 25;
|
||
|
||
19
|
||
|
||
20 /**
|
||
|
||
21 * The maximum number of unhandled exceptions to allow before failing.
|
||
|
||
22 *
|
||
|
||
23 * @var int
|
||
|
||
24 */
|
||
|
||
25 public $maxExceptions = 3;
|
||
|
||
26
|
||
|
||
27 /**
|
||
|
||
28 * Execute the job.
|
||
|
||
29 */
|
||
|
||
30 public function handle(): void
|
||
|
||
31 {
|
||
|
||
32 Redis::throttle('key')->allow(10)->every(60)->then(function () {
|
||
|
||
33 // Lock obtained, process the podcast...
|
||
|
||
34 }, function () {
|
||
|
||
35 // Unable to obtain lock...
|
||
|
||
36 return $this->release(10);
|
||
|
||
37 });
|
||
|
||
38 }
|
||
|
||
39}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
use Illuminate\Support\Facades\Redis;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
/**
|
||
* The number of times the job may be attempted.
|
||
*
|
||
* @var int
|
||
*/
|
||
public $tries = 25;
|
||
|
||
/**
|
||
* The maximum number of unhandled exceptions to allow before failing.
|
||
*
|
||
* @var int
|
||
*/
|
||
public $maxExceptions = 3;
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
Redis::throttle('key')->allow(10)->every(60)->then(function () {
|
||
// Lock obtained, process the podcast...
|
||
}, function () {
|
||
// Unable to obtain lock...
|
||
return $this->release(10);
|
||
});
|
||
}
|
||
}
|
||
|
||
In this example, the job is released for ten seconds if the application is
|
||
unable to obtain a Redis lock and will continue to be retried up to 25 times.
|
||
However, the job will fail if three unhandled exceptions are thrown by the
|
||
job.
|
||
|
||
#### Timeout
|
||
|
||
Often, you know roughly how long you expect your queued jobs to take. For this
|
||
reason, Laravel allows you to specify a "timeout" value. By default, the
|
||
timeout value is 60 seconds. If a job is processing for longer than the number
|
||
of seconds specified by the timeout value, the worker processing the job will
|
||
exit with an error. Typically, the worker will be restarted automatically by a
|
||
process manager configured on your server.
|
||
|
||
The maximum number of seconds that jobs can run may be specified using the
|
||
`--timeout` switch on the Artisan command line:
|
||
|
||
|
||
|
||
1php artisan queue:work --timeout=30
|
||
|
||
|
||
php artisan queue:work --timeout=30
|
||
|
||
If the job exceeds its maximum attempts by continually timing out, it will be
|
||
marked as failed.
|
||
|
||
You may also define the maximum number of seconds a job should be allowed to
|
||
run on the job class itself. If the timeout is specified on the job, it will
|
||
take precedence over any timeout specified on the command line:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5class ProcessPodcast implements ShouldQueue
|
||
|
||
6{
|
||
|
||
7 /**
|
||
|
||
8 * The number of seconds the job can run before timing out.
|
||
|
||
9 *
|
||
|
||
10 * @var int
|
||
|
||
11 */
|
||
|
||
12 public $timeout = 120;
|
||
|
||
13}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
/**
|
||
* The number of seconds the job can run before timing out.
|
||
*
|
||
* @var int
|
||
*/
|
||
public $timeout = 120;
|
||
}
|
||
|
||
Sometimes, IO blocking processes such as sockets or outgoing HTTP connections
|
||
may not respect your specified timeout. Therefore, when using these features,
|
||
you should always attempt to specify a timeout using their APIs as well. For
|
||
example, when using [Guzzle](https://docs.guzzlephp.org), you should always
|
||
specify a connection and request timeout value.
|
||
|
||
The [PCNTL](https://www.php.net/manual/en/book.pcntl.php) PHP extension must
|
||
be installed in order to specify job timeouts. In addition, a job's "timeout"
|
||
value should always be less than its "retry after" value. Otherwise, the job
|
||
may be re-attempted before it has actually finished executing or timed out.
|
||
|
||
#### Failing on Timeout
|
||
|
||
If you would like to indicate that a job should be marked as failed on
|
||
timeout, you may define the `$failOnTimeout` property on the job class:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Indicate if the job should be marked as failed on timeout.
|
||
|
||
3 *
|
||
|
||
4 * @var bool
|
||
|
||
5 */
|
||
|
||
6public $failOnTimeout = true;
|
||
|
||
|
||
/**
|
||
* Indicate if the job should be marked as failed on timeout.
|
||
*
|
||
* @var bool
|
||
*/
|
||
public $failOnTimeout = true;
|
||
|
||
By default, when a job times out, it consumes one attempt and is released back
|
||
to the queue (if retries are allowed). However, if you configure the job to
|
||
fail on timeout, it will not be retried, regardless of the value set for
|
||
tries.
|
||
|
||
### Error Handling
|
||
|
||
If an exception is thrown while the job is being processed, the job will
|
||
automatically be released back onto the queue so it may be attempted again.
|
||
The job will continue to be released until it has been attempted the maximum
|
||
number of times allowed by your application. The maximum number of attempts is
|
||
defined by the `--tries` switch used on the `queue:work` Artisan command.
|
||
Alternatively, the maximum number of attempts may be defined on the job class
|
||
itself. More information on running the queue worker can be found below.
|
||
|
||
#### Manually Releasing a Job
|
||
|
||
Sometimes you may wish to manually release a job back onto the queue so that
|
||
it can be attempted again at a later time. You may accomplish this by calling
|
||
the `release` method:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Execute the job.
|
||
|
||
3 */
|
||
|
||
4public function handle(): void
|
||
|
||
5{
|
||
|
||
6 // ...
|
||
|
||
7
|
||
|
||
8 $this->release();
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
// ...
|
||
|
||
$this->release();
|
||
}
|
||
|
||
By default, the `release` method will release the job back onto the queue for
|
||
immediate processing. However, you may instruct the queue to not make the job
|
||
available for processing until a given number of seconds has elapsed by
|
||
passing an integer or date instance to the `release` method:
|
||
|
||
|
||
|
||
1$this->release(10);
|
||
|
||
2
|
||
|
||
3$this->release(now()->addSeconds(10));
|
||
|
||
|
||
$this->release(10);
|
||
|
||
$this->release(now()->addSeconds(10));
|
||
|
||
#### Manually Failing a Job
|
||
|
||
Occasionally you may need to manually mark a job as "failed". To do so, you
|
||
may call the `fail` method:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Execute the job.
|
||
|
||
3 */
|
||
|
||
4public function handle(): void
|
||
|
||
5{
|
||
|
||
6 // ...
|
||
|
||
7
|
||
|
||
8 $this->fail();
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
// ...
|
||
|
||
$this->fail();
|
||
}
|
||
|
||
If you would like to mark your job as failed because of an exception that you
|
||
have caught, you may pass the exception to the `fail` method. Or, for
|
||
convenience, you may pass a string error message which will be converted to an
|
||
exception for you:
|
||
|
||
|
||
|
||
1$this->fail($exception);
|
||
|
||
2
|
||
|
||
3$this->fail('Something went wrong.');
|
||
|
||
|
||
$this->fail($exception);
|
||
|
||
$this->fail('Something went wrong.');
|
||
|
||
For more information on failed jobs, check out the documentation on dealing
|
||
with job failures.
|
||
|
||
#### Failing Jobs on Specific Exceptions
|
||
|
||
The `FailOnException` job middleware allows you to short-circuit retries when
|
||
specific exceptions are thrown. This allows retrying on transient exceptions
|
||
such as external API errors, but failing the job permanently on persistent
|
||
exceptions, such as a user's permissions being revoked:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use App\Models\User;
|
||
|
||
6use Illuminate\Auth\Access\AuthorizationException;
|
||
|
||
7use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
8use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
9use Illuminate\Queue\Middleware\FailOnException;
|
||
|
||
10use Illuminate\Support\Facades\Http;
|
||
|
||
11
|
||
|
||
12class SyncChatHistory implements ShouldQueue
|
||
|
||
13{
|
||
|
||
14 use Queueable;
|
||
|
||
15
|
||
|
||
16 public $tries = 3;
|
||
|
||
17
|
||
|
||
18 /**
|
||
|
||
19 * Create a new job instance.
|
||
|
||
20 */
|
||
|
||
21 public function __construct(
|
||
|
||
22 public User $user,
|
||
|
||
23 ) {}
|
||
|
||
24
|
||
|
||
25 /**
|
||
|
||
26 * Execute the job.
|
||
|
||
27 */
|
||
|
||
28 public function handle(): void
|
||
|
||
29 {
|
||
|
||
30 $user->authorize('sync-chat-history');
|
||
|
||
31
|
||
|
||
32 $response = Http::throw()->get(
|
||
|
||
33 "https://chat.laravel.test/?user={$user->uuid}"
|
||
|
||
34 );
|
||
|
||
35
|
||
|
||
36 // ...
|
||
|
||
37 }
|
||
|
||
38
|
||
|
||
39 /**
|
||
|
||
40 * Get the middleware the job should pass through.
|
||
|
||
41 */
|
||
|
||
42 public function middleware(): array
|
||
|
||
43 {
|
||
|
||
44 return [
|
||
|
||
45 new FailOnException([AuthorizationException::class])
|
||
|
||
46 ];
|
||
|
||
47 }
|
||
|
||
48}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Models\User;
|
||
use Illuminate\Auth\Access\AuthorizationException;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
use Illuminate\Queue\Middleware\FailOnException;
|
||
use Illuminate\Support\Facades\Http;
|
||
|
||
class SyncChatHistory implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
public $tries = 3;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
public User $user,
|
||
) {}
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
$user->authorize('sync-chat-history');
|
||
|
||
$response = Http::throw()->get(
|
||
"https://chat.laravel.test/?user={$user->uuid}"
|
||
);
|
||
|
||
// ...
|
||
}
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [
|
||
new FailOnException([AuthorizationException::class])
|
||
];
|
||
}
|
||
}
|
||
|
||
## Job Batching
|
||
|
||
Laravel's job batching feature allows you to easily execute a batch of jobs
|
||
and then perform some action when the batch of jobs has completed executing.
|
||
Before getting started, you should create a database migration to build a
|
||
table which will contain meta information about your job batches, such as
|
||
their completion percentage. This migration may be generated using the
|
||
`make:queue-batches-table` Artisan command:
|
||
|
||
|
||
|
||
1php artisan make:queue-batches-table
|
||
|
||
2
|
||
|
||
3php artisan migrate
|
||
|
||
|
||
php artisan make:queue-batches-table
|
||
|
||
php artisan migrate
|
||
|
||
### Defining Batchable Jobs
|
||
|
||
To define a batchable job, you should create a queueable job as normal;
|
||
however, you should add the `Illuminate\Bus\Batchable` trait to the job class.
|
||
This trait provides access to a `batch` method which may be used to retrieve
|
||
the current batch that the job is executing within:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use Illuminate\Bus\Batchable;
|
||
|
||
6use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
7use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
8
|
||
|
||
9class ImportCsv implements ShouldQueue
|
||
|
||
10{
|
||
|
||
11 use Batchable, Queueable;
|
||
|
||
12
|
||
|
||
13 /**
|
||
|
||
14 * Execute the job.
|
||
|
||
15 */
|
||
|
||
16 public function handle(): void
|
||
|
||
17 {
|
||
|
||
18 if ($this->batch()->cancelled()) {
|
||
|
||
19 // Determine if the batch has been cancelled...
|
||
|
||
20
|
||
|
||
21 return;
|
||
|
||
22 }
|
||
|
||
23
|
||
|
||
24 // Import a portion of the CSV file...
|
||
|
||
25 }
|
||
|
||
26}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use Illuminate\Bus\Batchable;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
class ImportCsv implements ShouldQueue
|
||
{
|
||
use Batchable, Queueable;
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
if ($this->batch()->cancelled()) {
|
||
// Determine if the batch has been cancelled...
|
||
|
||
return;
|
||
}
|
||
|
||
// Import a portion of the CSV file...
|
||
}
|
||
}
|
||
|
||
### Dispatching Batches
|
||
|
||
To dispatch a batch of jobs, you should use the `batch` method of the `Bus`
|
||
facade. Of course, batching is primarily useful when combined with completion
|
||
callbacks. So, you may use the `then`, `catch`, and `finally` methods to
|
||
define completion callbacks for the batch. Each of these callbacks will
|
||
receive an `Illuminate\Bus\Batch` instance when they are invoked. In this
|
||
example, we will imagine we are queueing a batch of jobs that each process a
|
||
given number of rows from a CSV file:
|
||
|
||
|
||
|
||
1use App\Jobs\ImportCsv;
|
||
|
||
2use Illuminate\Bus\Batch;
|
||
|
||
3use Illuminate\Support\Facades\Bus;
|
||
|
||
4use Throwable;
|
||
|
||
5
|
||
|
||
6$batch = Bus::batch([
|
||
|
||
7 new ImportCsv(1, 100),
|
||
|
||
8 new ImportCsv(101, 200),
|
||
|
||
9 new ImportCsv(201, 300),
|
||
|
||
10 new ImportCsv(301, 400),
|
||
|
||
11 new ImportCsv(401, 500),
|
||
|
||
12])->before(function (Batch $batch) {
|
||
|
||
13 // The batch has been created but no jobs have been added...
|
||
|
||
14})->progress(function (Batch $batch) {
|
||
|
||
15 // A single job has completed successfully...
|
||
|
||
16})->then(function (Batch $batch) {
|
||
|
||
17 // All jobs completed successfully...
|
||
|
||
18})->catch(function (Batch $batch, Throwable $e) {
|
||
|
||
19 // First batch job failure detected...
|
||
|
||
20})->finally(function (Batch $batch) {
|
||
|
||
21 // The batch has finished executing...
|
||
|
||
22})->dispatch();
|
||
|
||
23
|
||
|
||
24return $batch->id;
|
||
|
||
|
||
use App\Jobs\ImportCsv;
|
||
use Illuminate\Bus\Batch;
|
||
use Illuminate\Support\Facades\Bus;
|
||
use Throwable;
|
||
|
||
$batch = Bus::batch([
|
||
new ImportCsv(1, 100),
|
||
new ImportCsv(101, 200),
|
||
new ImportCsv(201, 300),
|
||
new ImportCsv(301, 400),
|
||
new ImportCsv(401, 500),
|
||
])->before(function (Batch $batch) {
|
||
// The batch has been created but no jobs have been added...
|
||
})->progress(function (Batch $batch) {
|
||
// A single job has completed successfully...
|
||
})->then(function (Batch $batch) {
|
||
// All jobs completed successfully...
|
||
})->catch(function (Batch $batch, Throwable $e) {
|
||
// First batch job failure detected...
|
||
})->finally(function (Batch $batch) {
|
||
// The batch has finished executing...
|
||
})->dispatch();
|
||
|
||
return $batch->id;
|
||
|
||
The batch's ID, which may be accessed via the `$batch->id` property, may be
|
||
used to query the Laravel command bus for information about the batch after it
|
||
has been dispatched.
|
||
|
||
Since batch callbacks are serialized and executed at a later time by the
|
||
Laravel queue, you should not use the `$this` variable within the callbacks.
|
||
In addition, since batched jobs are wrapped within database transactions,
|
||
database statements that trigger implicit commits should not be executed
|
||
within the jobs.
|
||
|
||
#### Naming Batches
|
||
|
||
Some tools such as [Laravel Horizon](/docs/12.x/horizon) and [Laravel
|
||
Telescope](/docs/12.x/telescope) may provide more user-friendly debug
|
||
information for batches if batches are named. To assign an arbitrary name to a
|
||
batch, you may call the `name` method while defining the batch:
|
||
|
||
|
||
|
||
1$batch = Bus::batch([
|
||
|
||
2 // ...
|
||
|
||
3])->then(function (Batch $batch) {
|
||
|
||
4 // All jobs completed successfully...
|
||
|
||
5})->name('Import CSV')->dispatch();
|
||
|
||
|
||
$batch = Bus::batch([
|
||
// ...
|
||
])->then(function (Batch $batch) {
|
||
// All jobs completed successfully...
|
||
})->name('Import CSV')->dispatch();
|
||
|
||
#### Batch Connection and Queue
|
||
|
||
If you would like to specify the connection and queue that should be used for
|
||
the batched jobs, you may use the `onConnection` and `onQueue` methods. All
|
||
batched jobs must execute within the same connection and queue:
|
||
|
||
|
||
|
||
1$batch = Bus::batch([
|
||
|
||
2 // ...
|
||
|
||
3])->then(function (Batch $batch) {
|
||
|
||
4 // All jobs completed successfully...
|
||
|
||
5})->onConnection('redis')->onQueue('imports')->dispatch();
|
||
|
||
|
||
$batch = Bus::batch([
|
||
// ...
|
||
])->then(function (Batch $batch) {
|
||
// All jobs completed successfully...
|
||
})->onConnection('redis')->onQueue('imports')->dispatch();
|
||
|
||
### Chains and Batches
|
||
|
||
You may define a set of chained jobs within a batch by placing the chained
|
||
jobs within an array. For example, we may execute two job chains in parallel
|
||
and execute a callback when both job chains have finished processing:
|
||
|
||
|
||
|
||
1use App\Jobs\ReleasePodcast;
|
||
|
||
2use App\Jobs\SendPodcastReleaseNotification;
|
||
|
||
3use Illuminate\Bus\Batch;
|
||
|
||
4use Illuminate\Support\Facades\Bus;
|
||
|
||
5
|
||
|
||
6Bus::batch([
|
||
|
||
7 [
|
||
|
||
8 new ReleasePodcast(1),
|
||
|
||
9 new SendPodcastReleaseNotification(1),
|
||
|
||
10 ],
|
||
|
||
11 [
|
||
|
||
12 new ReleasePodcast(2),
|
||
|
||
13 new SendPodcastReleaseNotification(2),
|
||
|
||
14 ],
|
||
|
||
15])->then(function (Batch $batch) {
|
||
|
||
16 // All jobs completed successfully...
|
||
|
||
17})->dispatch();
|
||
|
||
|
||
use App\Jobs\ReleasePodcast;
|
||
use App\Jobs\SendPodcastReleaseNotification;
|
||
use Illuminate\Bus\Batch;
|
||
use Illuminate\Support\Facades\Bus;
|
||
|
||
Bus::batch([
|
||
[
|
||
new ReleasePodcast(1),
|
||
new SendPodcastReleaseNotification(1),
|
||
],
|
||
[
|
||
new ReleasePodcast(2),
|
||
new SendPodcastReleaseNotification(2),
|
||
],
|
||
])->then(function (Batch $batch) {
|
||
// All jobs completed successfully...
|
||
})->dispatch();
|
||
|
||
Conversely, you may run batches of jobs within a chain by defining batches
|
||
within the chain. For example, you could first run a batch of jobs to release
|
||
multiple podcasts then a batch of jobs to send the release notifications:
|
||
|
||
|
||
|
||
1use App\Jobs\FlushPodcastCache;
|
||
|
||
2use App\Jobs\ReleasePodcast;
|
||
|
||
3use App\Jobs\SendPodcastReleaseNotification;
|
||
|
||
4use Illuminate\Support\Facades\Bus;
|
||
|
||
5
|
||
|
||
6Bus::chain([
|
||
|
||
7 new FlushPodcastCache,
|
||
|
||
8 Bus::batch([
|
||
|
||
9 new ReleasePodcast(1),
|
||
|
||
10 new ReleasePodcast(2),
|
||
|
||
11 ]),
|
||
|
||
12 Bus::batch([
|
||
|
||
13 new SendPodcastReleaseNotification(1),
|
||
|
||
14 new SendPodcastReleaseNotification(2),
|
||
|
||
15 ]),
|
||
|
||
16])->dispatch();
|
||
|
||
|
||
use App\Jobs\FlushPodcastCache;
|
||
use App\Jobs\ReleasePodcast;
|
||
use App\Jobs\SendPodcastReleaseNotification;
|
||
use Illuminate\Support\Facades\Bus;
|
||
|
||
Bus::chain([
|
||
new FlushPodcastCache,
|
||
Bus::batch([
|
||
new ReleasePodcast(1),
|
||
new ReleasePodcast(2),
|
||
]),
|
||
Bus::batch([
|
||
new SendPodcastReleaseNotification(1),
|
||
new SendPodcastReleaseNotification(2),
|
||
]),
|
||
])->dispatch();
|
||
|
||
### Adding Jobs to Batches
|
||
|
||
Sometimes it may be useful to add additional jobs to a batch from within a
|
||
batched job. This pattern can be useful when you need to batch thousands of
|
||
jobs which may take too long to dispatch during a web request. So, instead,
|
||
you may wish to dispatch an initial batch of "loader" jobs that hydrate the
|
||
batch with even more jobs:
|
||
|
||
|
||
|
||
1$batch = Bus::batch([
|
||
|
||
2 new LoadImportBatch,
|
||
|
||
3 new LoadImportBatch,
|
||
|
||
4 new LoadImportBatch,
|
||
|
||
5])->then(function (Batch $batch) {
|
||
|
||
6 // All jobs completed successfully...
|
||
|
||
7})->name('Import Contacts')->dispatch();
|
||
|
||
|
||
$batch = Bus::batch([
|
||
new LoadImportBatch,
|
||
new LoadImportBatch,
|
||
new LoadImportBatch,
|
||
])->then(function (Batch $batch) {
|
||
// All jobs completed successfully...
|
||
})->name('Import Contacts')->dispatch();
|
||
|
||
In this example, we will use the `LoadImportBatch` job to hydrate the batch
|
||
with additional jobs. To accomplish this, we may use the `add` method on the
|
||
batch instance that may be accessed via the job's `batch` method:
|
||
|
||
|
||
|
||
1use App\Jobs\ImportContacts;
|
||
|
||
2use Illuminate\Support\Collection;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Execute the job.
|
||
|
||
6 */
|
||
|
||
7public function handle(): void
|
||
|
||
8{
|
||
|
||
9 if ($this->batch()->cancelled()) {
|
||
|
||
10 return;
|
||
|
||
11 }
|
||
|
||
12
|
||
|
||
13 $this->batch()->add(Collection::times(1000, function () {
|
||
|
||
14 return new ImportContacts;
|
||
|
||
15 }));
|
||
|
||
16}
|
||
|
||
|
||
use App\Jobs\ImportContacts;
|
||
use Illuminate\Support\Collection;
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
if ($this->batch()->cancelled()) {
|
||
return;
|
||
}
|
||
|
||
$this->batch()->add(Collection::times(1000, function () {
|
||
return new ImportContacts;
|
||
}));
|
||
}
|
||
|
||
You may only add jobs to a batch from within a job that belongs to the same
|
||
batch.
|
||
|
||
### Inspecting Batches
|
||
|
||
The `Illuminate\Bus\Batch` instance that is provided to batch completion
|
||
callbacks has a variety of properties and methods to assist you in interacting
|
||
with and inspecting a given batch of jobs:
|
||
|
||
|
||
|
||
1// The UUID of the batch...
|
||
|
||
2$batch->id;
|
||
|
||
3
|
||
|
||
4// The name of the batch (if applicable)...
|
||
|
||
5$batch->name;
|
||
|
||
6
|
||
|
||
7// The number of jobs assigned to the batch...
|
||
|
||
8$batch->totalJobs;
|
||
|
||
9
|
||
|
||
10// The number of jobs that have not been processed by the queue...
|
||
|
||
11$batch->pendingJobs;
|
||
|
||
12
|
||
|
||
13// The number of jobs that have failed...
|
||
|
||
14$batch->failedJobs;
|
||
|
||
15
|
||
|
||
16// The number of jobs that have been processed thus far...
|
||
|
||
17$batch->processedJobs();
|
||
|
||
18
|
||
|
||
19// The completion percentage of the batch (0-100)...
|
||
|
||
20$batch->progress();
|
||
|
||
21
|
||
|
||
22// Indicates if the batch has finished executing...
|
||
|
||
23$batch->finished();
|
||
|
||
24
|
||
|
||
25// Cancel the execution of the batch...
|
||
|
||
26$batch->cancel();
|
||
|
||
27
|
||
|
||
28// Indicates if the batch has been cancelled...
|
||
|
||
29$batch->cancelled();
|
||
|
||
|
||
// The UUID of the batch...
|
||
$batch->id;
|
||
|
||
// The name of the batch (if applicable)...
|
||
$batch->name;
|
||
|
||
// The number of jobs assigned to the batch...
|
||
$batch->totalJobs;
|
||
|
||
// The number of jobs that have not been processed by the queue...
|
||
$batch->pendingJobs;
|
||
|
||
// The number of jobs that have failed...
|
||
$batch->failedJobs;
|
||
|
||
// The number of jobs that have been processed thus far...
|
||
$batch->processedJobs();
|
||
|
||
// The completion percentage of the batch (0-100)...
|
||
$batch->progress();
|
||
|
||
// Indicates if the batch has finished executing...
|
||
$batch->finished();
|
||
|
||
// Cancel the execution of the batch...
|
||
$batch->cancel();
|
||
|
||
// Indicates if the batch has been cancelled...
|
||
$batch->cancelled();
|
||
|
||
#### Returning Batches From Routes
|
||
|
||
All `Illuminate\Bus\Batch` instances are JSON serializable, meaning you can
|
||
return them directly from one of your application's routes to retrieve a JSON
|
||
payload containing information about the batch, including its completion
|
||
progress. This makes it convenient to display information about the batch's
|
||
completion progress in your application's UI.
|
||
|
||
To retrieve a batch by its ID, you may use the `Bus` facade's `findBatch`
|
||
method:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Bus;
|
||
|
||
2use Illuminate\Support\Facades\Route;
|
||
|
||
3
|
||
|
||
4Route::get('/batch/{batchId}', function (string $batchId) {
|
||
|
||
5 return Bus::findBatch($batchId);
|
||
|
||
6});
|
||
|
||
|
||
use Illuminate\Support\Facades\Bus;
|
||
use Illuminate\Support\Facades\Route;
|
||
|
||
Route::get('/batch/{batchId}', function (string $batchId) {
|
||
return Bus::findBatch($batchId);
|
||
});
|
||
|
||
### Cancelling Batches
|
||
|
||
Sometimes you may need to cancel a given batch's execution. This can be
|
||
accomplished by calling the `cancel` method on the `Illuminate\Bus\Batch`
|
||
instance:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Execute the job.
|
||
|
||
3 */
|
||
|
||
4public function handle(): void
|
||
|
||
5{
|
||
|
||
6 if ($this->user->exceedsImportLimit()) {
|
||
|
||
7 $this->batch()->cancel();
|
||
|
||
8
|
||
|
||
9 return;
|
||
|
||
10 }
|
||
|
||
11
|
||
|
||
12 if ($this->batch()->cancelled()) {
|
||
|
||
13 return;
|
||
|
||
14 }
|
||
|
||
15}
|
||
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
if ($this->user->exceedsImportLimit()) {
|
||
$this->batch()->cancel();
|
||
|
||
return;
|
||
}
|
||
|
||
if ($this->batch()->cancelled()) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
As you may have noticed in the previous examples, batched jobs should
|
||
typically determine if their corresponding batch has been cancelled before
|
||
continuing execution. However, for convenience, you may assign the
|
||
`SkipIfBatchCancelled` middleware to the job instead. As its name indicates,
|
||
this middleware will instruct Laravel to not process the job if its
|
||
corresponding batch has been cancelled:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Get the middleware the job should pass through.
|
||
|
||
5 */
|
||
|
||
6public function middleware(): array
|
||
|
||
7{
|
||
|
||
8 return [new SkipIfBatchCancelled];
|
||
|
||
9}
|
||
|
||
|
||
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
|
||
|
||
/**
|
||
* Get the middleware the job should pass through.
|
||
*/
|
||
public function middleware(): array
|
||
{
|
||
return [new SkipIfBatchCancelled];
|
||
}
|
||
|
||
### Batch Failures
|
||
|
||
When a batched job fails, the `catch` callback (if assigned) will be invoked.
|
||
This callback is only invoked for the first job that fails within the batch.
|
||
|
||
#### Allowing Failures
|
||
|
||
When a job within a batch fails, Laravel will automatically mark the batch as
|
||
"cancelled". If you wish, you may disable this behavior so that a job failure
|
||
does not automatically mark the batch as cancelled. This may be accomplished
|
||
by calling the `allowFailures` method while dispatching the batch:
|
||
|
||
|
||
|
||
1$batch = Bus::batch([
|
||
|
||
2 // ...
|
||
|
||
3])->then(function (Batch $batch) {
|
||
|
||
4 // All jobs completed successfully...
|
||
|
||
5})->allowFailures()->dispatch();
|
||
|
||
|
||
$batch = Bus::batch([
|
||
// ...
|
||
])->then(function (Batch $batch) {
|
||
// All jobs completed successfully...
|
||
})->allowFailures()->dispatch();
|
||
|
||
#### Retrying Failed Batch Jobs
|
||
|
||
For convenience, Laravel provides a `queue:retry-batch` Artisan command that
|
||
allows you to easily retry all of the failed jobs for a given batch. This
|
||
command accepts the UUID of the batch whose failed jobs should be retried:
|
||
|
||
|
||
|
||
1php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5
|
||
|
||
|
||
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5
|
||
|
||
### Pruning Batches
|
||
|
||
Without pruning, the `job_batches` table can accumulate records very quickly.
|
||
To mitigate this, you should [schedule](/docs/12.x/scheduling) the
|
||
`queue:prune-batches` Artisan command to run daily:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Schedule;
|
||
|
||
2
|
||
|
||
3Schedule::command('queue:prune-batches')->daily();
|
||
|
||
|
||
use Illuminate\Support\Facades\Schedule;
|
||
|
||
Schedule::command('queue:prune-batches')->daily();
|
||
|
||
By default, all finished batches that are more than 24 hours old will be
|
||
pruned. You may use the `hours` option when calling the command to determine
|
||
how long to retain batch data. For example, the following command will delete
|
||
all batches that finished over 48 hours ago:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Schedule;
|
||
|
||
2
|
||
|
||
3Schedule::command('queue:prune-batches --hours=48')->daily();
|
||
|
||
|
||
use Illuminate\Support\Facades\Schedule;
|
||
|
||
Schedule::command('queue:prune-batches --hours=48')->daily();
|
||
|
||
Sometimes, your `jobs_batches` table may accumulate batch records for batches
|
||
that never completed successfully, such as batches where a job failed and that
|
||
job was never retried successfully. You may instruct the `queue:prune-batches`
|
||
command to prune these unfinished batch records using the `unfinished` option:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Schedule;
|
||
|
||
2
|
||
|
||
3Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();
|
||
|
||
|
||
use Illuminate\Support\Facades\Schedule;
|
||
|
||
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();
|
||
|
||
Likewise, your `jobs_batches` table may also accumulate batch records for
|
||
cancelled batches. You may instruct the `queue:prune-batches` command to prune
|
||
these cancelled batch records using the `cancelled` option:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\Schedule;
|
||
|
||
2
|
||
|
||
3Schedule::command('queue:prune-batches --hours=48 --cancelled=72')->daily();
|
||
|
||
|
||
use Illuminate\Support\Facades\Schedule;
|
||
|
||
Schedule::command('queue:prune-batches --hours=48 --cancelled=72')->daily();
|
||
|
||
### Storing Batches in DynamoDB
|
||
|
||
Laravel also provides support for storing batch meta information in
|
||
[DynamoDB](https://aws.amazon.com/dynamodb) instead of a relational database.
|
||
However, you will need to manually create a DynamoDB table to store all of the
|
||
batch records.
|
||
|
||
Typically, this table should be named `job_batches`, but you should name the
|
||
table based on the value of the `queue.batching.table` configuration value
|
||
within your application's `queue` configuration file.
|
||
|
||
#### DynamoDB Batch Table Configuration
|
||
|
||
The `job_batches` table should have a string primary partition key named
|
||
`application` and a string primary sort key named `id`. The `application`
|
||
portion of the key will contain your application's name as defined by the
|
||
`name` configuration value within your application's `app` configuration file.
|
||
Since the application name is part of the DynamoDB table's key, you can use
|
||
the same table to store job batches for multiple Laravel applications.
|
||
|
||
In addition, you may define `ttl` attribute for your table if you would like
|
||
to take advantage of automatic batch pruning.
|
||
|
||
#### DynamoDB Configuration
|
||
|
||
Next, install the AWS SDK so that your Laravel application can communicate
|
||
with Amazon DynamoDB:
|
||
|
||
|
||
|
||
1composer require aws/aws-sdk-php
|
||
|
||
|
||
composer require aws/aws-sdk-php
|
||
|
||
Then, set the `queue.batching.driver` configuration option's value to
|
||
`dynamodb`. In addition, you should define `key`, `secret`, and `region`
|
||
configuration options within the `batching` configuration array. These options
|
||
will be used to authenticate with AWS. When using the `dynamodb` driver, the
|
||
`queue.batching.database` configuration option is unnecessary:
|
||
|
||
|
||
|
||
1'batching' => [
|
||
|
||
2 'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
|
||
|
||
3 'key' => env('AWS_ACCESS_KEY_ID'),
|
||
|
||
4 'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||
|
||
5 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||
|
||
6 'table' => 'job_batches',
|
||
|
||
7],
|
||
|
||
|
||
'batching' => [
|
||
'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
|
||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||
'table' => 'job_batches',
|
||
],
|
||
|
||
#### Pruning Batches in DynamoDB
|
||
|
||
When utilizing [DynamoDB](https://aws.amazon.com/dynamodb) to store job batch
|
||
information, the typical pruning commands used to prune batches stored in a
|
||
relational database will not work. Instead, you may utilize [DynamoDB's native
|
||
TTL
|
||
functionality](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html)
|
||
to automatically remove records for old batches.
|
||
|
||
If you defined your DynamoDB table with a `ttl` attribute, you may define
|
||
configuration parameters to instruct Laravel how to prune batch records. The
|
||
`queue.batching.ttl_attribute` configuration value defines the name of the
|
||
attribute holding the TTL, while the `queue.batching.ttl` configuration value
|
||
defines the number of seconds after which a batch record can be removed from
|
||
the DynamoDB table, relative to the last time the record was updated:
|
||
|
||
|
||
|
||
1'batching' => [
|
||
|
||
2 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
|
||
|
||
3 'key' => env('AWS_ACCESS_KEY_ID'),
|
||
|
||
4 'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||
|
||
5 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||
|
||
6 'table' => 'job_batches',
|
||
|
||
7 'ttl_attribute' => 'ttl',
|
||
|
||
8 'ttl' => 60 * 60 * 24 * 7, // 7 days...
|
||
|
||
9],
|
||
|
||
|
||
'batching' => [
|
||
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
|
||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||
'table' => 'job_batches',
|
||
'ttl_attribute' => 'ttl',
|
||
'ttl' => 60 * 60 * 24 * 7, // 7 days...
|
||
],
|
||
|
||
## Queueing Closures
|
||
|
||
Instead of dispatching a job class to the queue, you may also dispatch a
|
||
closure. This is great for quick, simple tasks that need to be executed
|
||
outside of the current request cycle. When dispatching closures to the queue,
|
||
the closure's code content is cryptographically signed so that it cannot be
|
||
modified in transit:
|
||
|
||
|
||
|
||
1use App\Models\Podcast;
|
||
|
||
2
|
||
|
||
3$podcast = Podcast::find(1);
|
||
|
||
4
|
||
|
||
5dispatch(function () use ($podcast) {
|
||
|
||
6 $podcast->publish();
|
||
|
||
7});
|
||
|
||
|
||
use App\Models\Podcast;
|
||
|
||
$podcast = Podcast::find(1);
|
||
|
||
dispatch(function () use ($podcast) {
|
||
$podcast->publish();
|
||
});
|
||
|
||
To assign a name to the queued closure which may be used by queue reporting
|
||
dashboards, as well as be displayed by the `queue:work` command, you may use
|
||
the `name` method:
|
||
|
||
|
||
|
||
1dispatch(function () {
|
||
|
||
2 // ...
|
||
|
||
3})->name('Publish Podcast');
|
||
|
||
|
||
dispatch(function () {
|
||
// ...
|
||
})->name('Publish Podcast');
|
||
|
||
Using the `catch` method, you may provide a closure that should be executed if
|
||
the queued closure fails to complete successfully after exhausting all of your
|
||
queue's configured retry attempts:
|
||
|
||
|
||
|
||
1use Throwable;
|
||
|
||
2
|
||
|
||
3dispatch(function () use ($podcast) {
|
||
|
||
4 $podcast->publish();
|
||
|
||
5})->catch(function (Throwable $e) {
|
||
|
||
6 // This job has failed...
|
||
|
||
7});
|
||
|
||
|
||
use Throwable;
|
||
|
||
dispatch(function () use ($podcast) {
|
||
$podcast->publish();
|
||
})->catch(function (Throwable $e) {
|
||
// This job has failed...
|
||
});
|
||
|
||
Since `catch` callbacks are serialized and executed at a later time by the
|
||
Laravel queue, you should not use the `$this` variable within `catch`
|
||
callbacks.
|
||
|
||
## Running the Queue Worker
|
||
|
||
### The `queue:work` Command
|
||
|
||
Laravel includes an Artisan command that will start a queue worker and process
|
||
new jobs as they are pushed onto the queue. You may run the worker using the
|
||
`queue:work` Artisan command. Note that once the `queue:work` command has
|
||
started, it will continue to run until it is manually stopped or you close
|
||
your terminal:
|
||
|
||
|
||
|
||
1php artisan queue:work
|
||
|
||
|
||
php artisan queue:work
|
||
|
||
To keep the `queue:work` process running permanently in the background, you
|
||
should use a process monitor such as Supervisor to ensure that the queue
|
||
worker does not stop running.
|
||
|
||
You may include the `-v` flag when invoking the `queue:work` command if you
|
||
would like the processed job IDs, connection names, and queue names to be
|
||
included in the command's output:
|
||
|
||
|
||
|
||
1php artisan queue:work -v
|
||
|
||
|
||
php artisan queue:work -v
|
||
|
||
Remember, queue workers are long-lived processes and store the booted
|
||
application state in memory. As a result, they will not notice changes in your
|
||
code base after they have been started. So, during your deployment process, be
|
||
sure to restart your queue workers. In addition, remember that any static
|
||
state created or modified by your application will not be automatically reset
|
||
between jobs.
|
||
|
||
Alternatively, you may run the `queue:listen` command. When using the
|
||
`queue:listen` command, you don't have to manually restart the worker when you
|
||
want to reload your updated code or reset the application state; however, this
|
||
command is significantly less efficient than the `queue:work` command:
|
||
|
||
|
||
|
||
1php artisan queue:listen
|
||
|
||
|
||
php artisan queue:listen
|
||
|
||
#### Running Multiple Queue Workers
|
||
|
||
To assign multiple workers to a queue and process jobs concurrently, you
|
||
should simply start multiple `queue:work` processes. This can either be done
|
||
locally via multiple tabs in your terminal or in production using your process
|
||
manager's configuration settings. When using Supervisor, you may use the
|
||
`numprocs` configuration value.
|
||
|
||
#### Specifying the Connection and Queue
|
||
|
||
You may also specify which queue connection the worker should utilize. The
|
||
connection name passed to the `work` command should correspond to one of the
|
||
connections defined in your `config/queue.php` configuration file:
|
||
|
||
|
||
|
||
1php artisan queue:work redis
|
||
|
||
|
||
php artisan queue:work redis
|
||
|
||
By default, the `queue:work` command only processes jobs for the default queue
|
||
on a given connection. However, you may customize your queue worker even
|
||
further by only processing particular queues for a given connection. For
|
||
example, if all of your emails are processed in an `emails` queue on your
|
||
`redis` queue connection, you may issue the following command to start a
|
||
worker that only processes that queue:
|
||
|
||
|
||
|
||
1php artisan queue:work redis --queue=emails
|
||
|
||
|
||
php artisan queue:work redis --queue=emails
|
||
|
||
#### Processing a Specified Number of Jobs
|
||
|
||
The `--once` option may be used to instruct the worker to only process a
|
||
single job from the queue:
|
||
|
||
|
||
|
||
1php artisan queue:work --once
|
||
|
||
|
||
php artisan queue:work --once
|
||
|
||
The `--max-jobs` option may be used to instruct the worker to process the
|
||
given number of jobs and then exit. This option may be useful when combined
|
||
with Supervisor so that your workers are automatically restarted after
|
||
processing a given number of jobs, releasing any memory they may have
|
||
accumulated:
|
||
|
||
|
||
|
||
1php artisan queue:work --max-jobs=1000
|
||
|
||
|
||
php artisan queue:work --max-jobs=1000
|
||
|
||
#### Processing All Queued Jobs and Then Exiting
|
||
|
||
The `--stop-when-empty` option may be used to instruct the worker to process
|
||
all jobs and then exit gracefully. This option can be useful when processing
|
||
Laravel queues within a Docker container if you wish to shutdown the container
|
||
after the queue is empty:
|
||
|
||
|
||
|
||
1php artisan queue:work --stop-when-empty
|
||
|
||
|
||
php artisan queue:work --stop-when-empty
|
||
|
||
#### Processing Jobs for a Given Number of Seconds
|
||
|
||
The `--max-time` option may be used to instruct the worker to process jobs for
|
||
the given number of seconds and then exit. This option may be useful when
|
||
combined with Supervisor so that your workers are automatically restarted
|
||
after processing jobs for a given amount of time, releasing any memory they
|
||
may have accumulated:
|
||
|
||
|
||
|
||
1# Process jobs for one hour and then exit...
|
||
|
||
2php artisan queue:work --max-time=3600
|
||
|
||
|
||
# Process jobs for one hour and then exit...
|
||
php artisan queue:work --max-time=3600
|
||
|
||
#### Worker Sleep Duration
|
||
|
||
When jobs are available on the queue, the worker will keep processing jobs
|
||
with no delay in between jobs. However, the `sleep` option determines how many
|
||
seconds the worker will "sleep" if there are no jobs available. Of course,
|
||
while sleeping, the worker will not process any new jobs:
|
||
|
||
|
||
|
||
1php artisan queue:work --sleep=3
|
||
|
||
|
||
php artisan queue:work --sleep=3
|
||
|
||
#### Maintenance Mode and Queues
|
||
|
||
While your application is in [maintenance
|
||
mode](/docs/12.x/configuration#maintenance-mode), no queued jobs will be
|
||
handled. The jobs will continue to be handled as normal once the application
|
||
is out of maintenance mode.
|
||
|
||
To force your queue workers to process jobs even if maintenance mode is
|
||
enabled, you may use `--force` option:
|
||
|
||
|
||
|
||
1php artisan queue:work --force
|
||
|
||
|
||
php artisan queue:work --force
|
||
|
||
#### Resource Considerations
|
||
|
||
Daemon queue workers do not "reboot" the framework before processing each job.
|
||
Therefore, you should release any heavy resources after each job completes.
|
||
For example, if you are doing image manipulation with the [GD
|
||
library](https://www.php.net/manual/en/book.image.php), you should free the
|
||
memory with `imagedestroy` when you are done processing the image.
|
||
|
||
### Queue Priorities
|
||
|
||
Sometimes you may wish to prioritize how your queues are processed. For
|
||
example, in your `config/queue.php` configuration file, you may set the
|
||
default `queue` for your `redis` connection to `low`. However, occasionally
|
||
you may wish to push a job to a `high` priority queue like so:
|
||
|
||
|
||
|
||
1dispatch((new Job)->onQueue('high'));
|
||
|
||
|
||
dispatch((new Job)->onQueue('high'));
|
||
|
||
To start a worker that verifies that all of the `high` queue jobs are
|
||
processed before continuing to any jobs on the `low` queue, pass a comma-
|
||
delimited list of queue names to the `work` command:
|
||
|
||
|
||
|
||
1php artisan queue:work --queue=high,low
|
||
|
||
|
||
php artisan queue:work --queue=high,low
|
||
|
||
### Queue Workers and Deployment
|
||
|
||
Since queue workers are long-lived processes, they will not notice changes to
|
||
your code without being restarted. So, the simplest way to deploy an
|
||
application using queue workers is to restart the workers during your
|
||
deployment process. You may gracefully restart all of the workers by issuing
|
||
the `queue:restart` command:
|
||
|
||
|
||
|
||
1php artisan queue:restart
|
||
|
||
|
||
php artisan queue:restart
|
||
|
||
This command will instruct all queue workers to gracefully exit after they
|
||
finish processing their current job so that no existing jobs are lost. Since
|
||
the queue workers will exit when the `queue:restart` command is executed, you
|
||
should be running a process manager such as Supervisor to automatically
|
||
restart the queue workers.
|
||
|
||
The queue uses the [cache](/docs/12.x/cache) to store restart signals, so you
|
||
should verify that a cache driver is properly configured for your application
|
||
before using this feature.
|
||
|
||
### Job Expirations and Timeouts
|
||
|
||
#### Job Expiration
|
||
|
||
In your `config/queue.php` configuration file, each queue connection defines a
|
||
`retry_after` option. This option specifies how many seconds the queue
|
||
connection should wait before retrying a job that is being processed. For
|
||
example, if the value of `retry_after` is set to `90`, the job will be
|
||
released back onto the queue if it has been processing for 90 seconds without
|
||
being released or deleted. Typically, you should set the `retry_after` value
|
||
to the maximum number of seconds your jobs should reasonably take to complete
|
||
processing.
|
||
|
||
The only queue connection which does not contain a `retry_after` value is
|
||
Amazon SQS. SQS will retry the job based on the [Default Visibility
|
||
Timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/AboutVT.html)
|
||
which is managed within the AWS console.
|
||
|
||
#### Worker Timeouts
|
||
|
||
The `queue:work` Artisan command exposes a `--timeout` option. By default, the
|
||
`--timeout` value is 60 seconds. If a job is processing for longer than the
|
||
number of seconds specified by the timeout value, the worker processing the
|
||
job will exit with an error. Typically, the worker will be restarted
|
||
automatically by a process manager configured on your server:
|
||
|
||
|
||
|
||
1php artisan queue:work --timeout=60
|
||
|
||
|
||
php artisan queue:work --timeout=60
|
||
|
||
The `retry_after` configuration option and the `--timeout` CLI option are
|
||
different, but work together to ensure that jobs are not lost and that jobs
|
||
are only successfully processed once.
|
||
|
||
The `--timeout` value should always be at least several seconds shorter than
|
||
your `retry_after` configuration value. This will ensure that a worker
|
||
processing a frozen job is always terminated before the job is retried. If
|
||
your `--timeout` option is longer than your `retry_after` configuration value,
|
||
your jobs may be processed twice.
|
||
|
||
## Supervisor Configuration
|
||
|
||
In production, you need a way to keep your `queue:work` processes running. A
|
||
`queue:work` process may stop running for a variety of reasons, such as an
|
||
exceeded worker timeout or the execution of the `queue:restart` command.
|
||
|
||
For this reason, you need to configure a process monitor that can detect when
|
||
your `queue:work` processes exit and automatically restart them. In addition,
|
||
process monitors can allow you to specify how many `queue:work` processes you
|
||
would like to run concurrently. Supervisor is a process monitor commonly used
|
||
in Linux environments and we will discuss how to configure it in the following
|
||
documentation.
|
||
|
||
#### Installing Supervisor
|
||
|
||
Supervisor is a process monitor for the Linux operating system, and will
|
||
automatically restart your `queue:work` processes if they fail. To install
|
||
Supervisor on Ubuntu, you may use the following command:
|
||
|
||
|
||
|
||
1sudo apt-get install supervisor
|
||
|
||
|
||
sudo apt-get install supervisor
|
||
|
||
If configuring and managing Supervisor yourself sounds overwhelming, consider
|
||
using [Laravel Cloud](https://cloud.laravel.com), which provides a fully-
|
||
managed platform for running Laravel queue workers.
|
||
|
||
#### Configuring Supervisor
|
||
|
||
Supervisor configuration files are typically stored in the
|
||
`/etc/supervisor/conf.d` directory. Within this directory, you may create any
|
||
number of configuration files that instruct supervisor how your processes
|
||
should be monitored. For example, let's create a `laravel-worker.conf` file
|
||
that starts and monitors `queue:work` processes:
|
||
|
||
|
||
|
||
1[program:laravel-worker]
|
||
|
||
2process_name=%(program_name)s_%(process_num)02d
|
||
|
||
3command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600
|
||
|
||
4autostart=true
|
||
|
||
5autorestart=true
|
||
|
||
6stopasgroup=true
|
||
|
||
7killasgroup=true
|
||
|
||
8user=forge
|
||
|
||
9numprocs=8
|
||
|
||
10redirect_stderr=true
|
||
|
||
11stdout_logfile=/home/forge/app.com/worker.log
|
||
|
||
12stopwaitsecs=3600
|
||
|
||
|
||
[program:laravel-worker]
|
||
process_name=%(program_name)s_%(process_num)02d
|
||
command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600
|
||
autostart=true
|
||
autorestart=true
|
||
stopasgroup=true
|
||
killasgroup=true
|
||
user=forge
|
||
numprocs=8
|
||
redirect_stderr=true
|
||
stdout_logfile=/home/forge/app.com/worker.log
|
||
stopwaitsecs=3600
|
||
|
||
In this example, the `numprocs` directive will instruct Supervisor to run
|
||
eight `queue:work` processes and monitor all of them, automatically restarting
|
||
them if they fail. You should change the `command` directive of the
|
||
configuration to reflect your desired queue connection and worker options.
|
||
|
||
You should ensure that the value of `stopwaitsecs` is greater than the number
|
||
of seconds consumed by your longest running job. Otherwise, Supervisor may
|
||
kill the job before it is finished processing.
|
||
|
||
#### Starting Supervisor
|
||
|
||
Once the configuration file has been created, you may update the Supervisor
|
||
configuration and start the processes using the following commands:
|
||
|
||
|
||
|
||
1sudo supervisorctl reread
|
||
|
||
2
|
||
|
||
3sudo supervisorctl update
|
||
|
||
4
|
||
|
||
5sudo supervisorctl start "laravel-worker:*"
|
||
|
||
|
||
sudo supervisorctl reread
|
||
|
||
sudo supervisorctl update
|
||
|
||
sudo supervisorctl start "laravel-worker:*"
|
||
|
||
For more information on Supervisor, consult the [Supervisor
|
||
documentation](http://supervisord.org/index.html).
|
||
|
||
## Dealing With Failed Jobs
|
||
|
||
Sometimes your queued jobs will fail. Don't worry, things don't always go as
|
||
planned! Laravel includes a convenient way to specify the maximum number of
|
||
times a job should be attempted. After an asynchronous job has exceeded this
|
||
number of attempts, it will be inserted into the `failed_jobs` database table.
|
||
[Synchronously dispatched jobs](/docs/12.x/queues#synchronous-dispatching)
|
||
that fail are not stored in this table and their exceptions are immediately
|
||
handled by the application.
|
||
|
||
A migration to create the `failed_jobs` table is typically already present in
|
||
new Laravel applications. However, if your application does not contain a
|
||
migration for this table, you may use the `make:queue-failed-table` command to
|
||
create the migration:
|
||
|
||
|
||
|
||
1php artisan make:queue-failed-table
|
||
|
||
2
|
||
|
||
3php artisan migrate
|
||
|
||
|
||
php artisan make:queue-failed-table
|
||
|
||
php artisan migrate
|
||
|
||
When running a queue worker process, you may specify the maximum number of
|
||
times a job should be attempted using the `--tries` switch on the `queue:work`
|
||
command. If you do not specify a value for the `--tries` option, jobs will
|
||
only be attempted once or as many times as specified by the job class'
|
||
`$tries` property:
|
||
|
||
|
||
|
||
1php artisan queue:work redis --tries=3
|
||
|
||
|
||
php artisan queue:work redis --tries=3
|
||
|
||
Using the `--backoff` option, you may specify how many seconds Laravel should
|
||
wait before retrying a job that has encountered an exception. By default, a
|
||
job is immediately released back onto the queue so that it may be attempted
|
||
again:
|
||
|
||
|
||
|
||
1php artisan queue:work redis --tries=3 --backoff=3
|
||
|
||
|
||
php artisan queue:work redis --tries=3 --backoff=3
|
||
|
||
If you would like to configure how many seconds Laravel should wait before
|
||
retrying a job that has encountered an exception on a per-job basis, you may
|
||
do so by defining a `backoff` property on your job class:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * The number of seconds to wait before retrying the job.
|
||
|
||
3 *
|
||
|
||
4 * @var int
|
||
|
||
5 */
|
||
|
||
6public $backoff = 3;
|
||
|
||
|
||
/**
|
||
* The number of seconds to wait before retrying the job.
|
||
*
|
||
* @var int
|
||
*/
|
||
public $backoff = 3;
|
||
|
||
If you require more complex logic for determining the job's backoff time, you
|
||
may define a `backoff` method on your job class:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Calculate the number of seconds to wait before retrying the job.
|
||
|
||
3 */
|
||
|
||
4public function backoff(): int
|
||
|
||
5{
|
||
|
||
6 return 3;
|
||
|
||
7}
|
||
|
||
|
||
/**
|
||
* Calculate the number of seconds to wait before retrying the job.
|
||
*/
|
||
public function backoff(): int
|
||
{
|
||
return 3;
|
||
}
|
||
|
||
You may easily configure "exponential" backoffs by returning an array of
|
||
backoff values from the `backoff` method. In this example, the retry delay
|
||
will be 1 second for the first retry, 5 seconds for the second retry, 10
|
||
seconds for the third retry, and 10 seconds for every subsequent retry if
|
||
there are more attempts remaining:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Calculate the number of seconds to wait before retrying the job.
|
||
|
||
3 *
|
||
|
||
4 * @return array<int, int>
|
||
|
||
5 */
|
||
|
||
6public function backoff(): array
|
||
|
||
7{
|
||
|
||
8 return [1, 5, 10];
|
||
|
||
9}
|
||
|
||
|
||
/**
|
||
* Calculate the number of seconds to wait before retrying the job.
|
||
*
|
||
* @return array<int, int>
|
||
*/
|
||
public function backoff(): array
|
||
{
|
||
return [1, 5, 10];
|
||
}
|
||
|
||
### Cleaning Up After Failed Jobs
|
||
|
||
When a particular job fails, you may want to send an alert to your users or
|
||
revert any actions that were partially completed by the job. To accomplish
|
||
this, you may define a `failed` method on your job class. The `Throwable`
|
||
instance that caused the job to fail will be passed to the `failed` method:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Jobs;
|
||
|
||
4
|
||
|
||
5use App\Models\Podcast;
|
||
|
||
6use App\Services\AudioProcessor;
|
||
|
||
7use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
8use Illuminate\Foundation\Queue\Queueable;
|
||
|
||
9use Throwable;
|
||
|
||
10
|
||
|
||
11class ProcessPodcast implements ShouldQueue
|
||
|
||
12{
|
||
|
||
13 use Queueable;
|
||
|
||
14
|
||
|
||
15 /**
|
||
|
||
16 * Create a new job instance.
|
||
|
||
17 */
|
||
|
||
18 public function __construct(
|
||
|
||
19 public Podcast $podcast,
|
||
|
||
20 ) {}
|
||
|
||
21
|
||
|
||
22 /**
|
||
|
||
23 * Execute the job.
|
||
|
||
24 */
|
||
|
||
25 public function handle(AudioProcessor $processor): void
|
||
|
||
26 {
|
||
|
||
27 // Process uploaded podcast...
|
||
|
||
28 }
|
||
|
||
29
|
||
|
||
30 /**
|
||
|
||
31 * Handle a job failure.
|
||
|
||
32 */
|
||
|
||
33 public function failed(?Throwable $exception): void
|
||
|
||
34 {
|
||
|
||
35 // Send user notification of failure, etc...
|
||
|
||
36 }
|
||
|
||
37}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Models\Podcast;
|
||
use App\Services\AudioProcessor;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
use Throwable;
|
||
|
||
class ProcessPodcast implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
public Podcast $podcast,
|
||
) {}
|
||
|
||
/**
|
||
* Execute the job.
|
||
*/
|
||
public function handle(AudioProcessor $processor): void
|
||
{
|
||
// Process uploaded podcast...
|
||
}
|
||
|
||
/**
|
||
* Handle a job failure.
|
||
*/
|
||
public function failed(?Throwable $exception): void
|
||
{
|
||
// Send user notification of failure, etc...
|
||
}
|
||
}
|
||
|
||
A new instance of the job is instantiated before invoking the `failed` method;
|
||
therefore, any class property modifications that may have occurred within the
|
||
`handle` method will be lost.
|
||
|
||
A failed job is not necessarily one that encountered an unhandled exception. A
|
||
job may also be considered failed when it has exhausted all of its allowed
|
||
attempts. These attempts can be consumed in several ways:
|
||
|
||
* The job timed out.
|
||
* The job encounters an unhandled exception during execution.
|
||
* The job is released back to the queue either manually or by a middleware.
|
||
|
||
If the final attempt fails due to an exception thrown during job execution,
|
||
that exception will be passed to the job's `failed` method. However, if the
|
||
job fails because it has reached the maximum number of allowed attempts, the
|
||
`$exception` will be an instance of
|
||
`Illuminate\Queue\MaxAttemptsExceededException`. Similarly, if the job fails
|
||
due to exceeding the configured timeout, the `$exception` will be an instance
|
||
of `Illuminate\Queue\TimeoutExceededException`.
|
||
|
||
### Retrying Failed Jobs
|
||
|
||
To view all of the failed jobs that have been inserted into your `failed_jobs`
|
||
database table, you may use the `queue:failed` Artisan command:
|
||
|
||
|
||
|
||
1php artisan queue:failed
|
||
|
||
|
||
php artisan queue:failed
|
||
|
||
The `queue:failed` command will list the job ID, connection, queue, failure
|
||
time, and other information about the job. The job ID may be used to retry the
|
||
failed job. For instance, to retry a failed job that has an ID of
|
||
`ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece`, issue the following command:
|
||
|
||
|
||
|
||
1php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
|
||
|
||
|
||
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
|
||
|
||
If necessary, you may pass multiple IDs to the command:
|
||
|
||
|
||
|
||
1php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d
|
||
|
||
|
||
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d
|
||
|
||
You may also retry all of the failed jobs for a particular queue:
|
||
|
||
|
||
|
||
1php artisan queue:retry --queue=name
|
||
|
||
|
||
php artisan queue:retry --queue=name
|
||
|
||
To retry all of your failed jobs, execute the `queue:retry` command and pass
|
||
`all` as the ID:
|
||
|
||
|
||
|
||
1php artisan queue:retry all
|
||
|
||
|
||
php artisan queue:retry all
|
||
|
||
If you would like to delete a failed job, you may use the `queue:forget`
|
||
command:
|
||
|
||
|
||
|
||
1php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d
|
||
|
||
|
||
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d
|
||
|
||
When using [Horizon](/docs/12.x/horizon), you should use the `horizon:forget`
|
||
command to delete a failed job instead of the `queue:forget` command.
|
||
|
||
To delete all of your failed jobs from the `failed_jobs` table, you may use
|
||
the `queue:flush` command:
|
||
|
||
|
||
|
||
1php artisan queue:flush
|
||
|
||
|
||
php artisan queue:flush
|
||
|
||
The `queue:flush` command removes all failed job records from your queue, no
|
||
matter how old the failed job is. You may use the `--hours` option to only
|
||
delete jobs that failed a certain number of hours ago or earlier:
|
||
|
||
|
||
|
||
1php artisan queue:flush --hours=48
|
||
|
||
|
||
php artisan queue:flush --hours=48
|
||
|
||
### Ignoring Missing Models
|
||
|
||
When injecting an Eloquent model into a job, the model is automatically
|
||
serialized before being placed on the queue and re-retrieved from the database
|
||
when the job is processed. However, if the model has been deleted while the
|
||
job was waiting to be processed by a worker, your job may fail with a
|
||
`ModelNotFoundException`.
|
||
|
||
For convenience, you may choose to automatically delete jobs with missing
|
||
models by setting your job's `deleteWhenMissingModels` property to `true`.
|
||
When this property is set to `true`, Laravel will quietly discard the job
|
||
without raising an exception:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Delete the job if its models no longer exist.
|
||
|
||
3 *
|
||
|
||
4 * @var bool
|
||
|
||
5 */
|
||
|
||
6public $deleteWhenMissingModels = true;
|
||
|
||
|
||
/**
|
||
* Delete the job if its models no longer exist.
|
||
*
|
||
* @var bool
|
||
*/
|
||
public $deleteWhenMissingModels = true;
|
||
|
||
### Pruning Failed Jobs
|
||
|
||
You may prune the records in your application's `failed_jobs` table by
|
||
invoking the `queue:prune-failed` Artisan command:
|
||
|
||
|
||
|
||
1php artisan queue:prune-failed
|
||
|
||
|
||
php artisan queue:prune-failed
|
||
|
||
By default, all the failed job records that are more than 24 hours old will be
|
||
pruned. If you provide the `--hours` option to the command, only the failed
|
||
job records that were inserted within the last N number of hours will be
|
||
retained. For example, the following command will delete all the failed job
|
||
records that were inserted more than 48 hours ago:
|
||
|
||
|
||
|
||
1php artisan queue:prune-failed --hours=48
|
||
|
||
|
||
php artisan queue:prune-failed --hours=48
|
||
|
||
### Storing Failed Jobs in DynamoDB
|
||
|
||
Laravel also provides support for storing your failed job records in
|
||
[DynamoDB](https://aws.amazon.com/dynamodb) instead of a relational database
|
||
table. However, you must manually create a DynamoDB table to store all of the
|
||
failed job records. Typically, this table should be named `failed_jobs`, but
|
||
you should name the table based on the value of the `queue.failed.table`
|
||
configuration value within your application's `queue` configuration file.
|
||
|
||
The `failed_jobs` table should have a string primary partition key named
|
||
`application` and a string primary sort key named `uuid`. The `application`
|
||
portion of the key will contain your application's name as defined by the
|
||
`name` configuration value within your application's `app` configuration file.
|
||
Since the application name is part of the DynamoDB table's key, you can use
|
||
the same table to store failed jobs for multiple Laravel applications.
|
||
|
||
In addition, ensure that you install the AWS SDK so that your Laravel
|
||
application can communicate with Amazon DynamoDB:
|
||
|
||
|
||
|
||
1composer require aws/aws-sdk-php
|
||
|
||
|
||
composer require aws/aws-sdk-php
|
||
|
||
Next, set the `queue.failed.driver` configuration option's value to
|
||
`dynamodb`. In addition, you should define `key`, `secret`, and `region`
|
||
configuration options within the failed job configuration array. These options
|
||
will be used to authenticate with AWS. When using the `dynamodb` driver, the
|
||
`queue.failed.database` configuration option is unnecessary:
|
||
|
||
|
||
|
||
1'failed' => [
|
||
|
||
2 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
|
||
|
||
3 'key' => env('AWS_ACCESS_KEY_ID'),
|
||
|
||
4 'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||
|
||
5 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||
|
||
6 'table' => 'failed_jobs',
|
||
|
||
7],
|
||
|
||
|
||
'failed' => [
|
||
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
|
||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||
'table' => 'failed_jobs',
|
||
],
|
||
|
||
### Disabling Failed Job Storage
|
||
|
||
You may instruct Laravel to discard failed jobs without storing them by
|
||
setting the `queue.failed.driver` configuration option's value to `null`.
|
||
Typically, this may be accomplished via the `QUEUE_FAILED_DRIVER` environment
|
||
variable:
|
||
|
||
|
||
|
||
1QUEUE_FAILED_DRIVER=null
|
||
|
||
|
||
QUEUE_FAILED_DRIVER=null
|
||
|
||
### Failed Job Events
|
||
|
||
If you would like to register an event listener that will be invoked when a
|
||
job fails, you may use the `Queue` facade's `failing` method. For example, we
|
||
may attach a closure to this event from the `boot` method of the
|
||
`AppServiceProvider` that is included with Laravel:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Providers;
|
||
|
||
4
|
||
|
||
5use Illuminate\Support\Facades\Queue;
|
||
|
||
6use Illuminate\Support\ServiceProvider;
|
||
|
||
7use Illuminate\Queue\Events\JobFailed;
|
||
|
||
8
|
||
|
||
9class AppServiceProvider extends ServiceProvider
|
||
|
||
10{
|
||
|
||
11 /**
|
||
|
||
12 * Register any application services.
|
||
|
||
13 */
|
||
|
||
14 public function register(): void
|
||
|
||
15 {
|
||
|
||
16 // ...
|
||
|
||
17 }
|
||
|
||
18
|
||
|
||
19 /**
|
||
|
||
20 * Bootstrap any application services.
|
||
|
||
21 */
|
||
|
||
22 public function boot(): void
|
||
|
||
23 {
|
||
|
||
24 Queue::failing(function (JobFailed $event) {
|
||
|
||
25 // $event->connectionName
|
||
|
||
26 // $event->job
|
||
|
||
27 // $event->exception
|
||
|
||
28 });
|
||
|
||
29 }
|
||
|
||
30}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Providers;
|
||
|
||
use Illuminate\Support\Facades\Queue;
|
||
use Illuminate\Support\ServiceProvider;
|
||
use Illuminate\Queue\Events\JobFailed;
|
||
|
||
class AppServiceProvider extends ServiceProvider
|
||
{
|
||
/**
|
||
* Register any application services.
|
||
*/
|
||
public function register(): void
|
||
{
|
||
// ...
|
||
}
|
||
|
||
/**
|
||
* Bootstrap any application services.
|
||
*/
|
||
public function boot(): void
|
||
{
|
||
Queue::failing(function (JobFailed $event) {
|
||
// $event->connectionName
|
||
// $event->job
|
||
// $event->exception
|
||
});
|
||
}
|
||
}
|
||
|
||
## Clearing Jobs From Queues
|
||
|
||
When using [Horizon](/docs/12.x/horizon), you should use the `horizon:clear`
|
||
command to clear jobs from the queue instead of the `queue:clear` command.
|
||
|
||
If you would like to delete all jobs from the default queue of the default
|
||
connection, you may do so using the `queue:clear` Artisan command:
|
||
|
||
|
||
|
||
1php artisan queue:clear
|
||
|
||
|
||
php artisan queue:clear
|
||
|
||
You may also provide the `connection` argument and `queue` option to delete
|
||
jobs from a specific connection and queue:
|
||
|
||
|
||
|
||
1php artisan queue:clear redis --queue=emails
|
||
|
||
|
||
php artisan queue:clear redis --queue=emails
|
||
|
||
Clearing jobs from queues is only available for the SQS, Redis, and database
|
||
queue drivers. In addition, the SQS message deletion process takes up to 60
|
||
seconds, so jobs sent to the SQS queue up to 60 seconds after you clear the
|
||
queue might also be deleted.
|
||
|
||
## Monitoring Your Queues
|
||
|
||
If your queue receives a sudden influx of jobs, it could become overwhelmed,
|
||
leading to a long wait time for jobs to complete. If you wish, Laravel can
|
||
alert you when your queue job count exceeds a specified threshold.
|
||
|
||
To get started, you should schedule the `queue:monitor` command to [run every
|
||
minute](/docs/12.x/scheduling). The command accepts the names of the queues
|
||
you wish to monitor as well as your desired job count threshold:
|
||
|
||
|
||
|
||
1php artisan queue:monitor redis:default,redis:deployments --max=100
|
||
|
||
|
||
php artisan queue:monitor redis:default,redis:deployments --max=100
|
||
|
||
Scheduling this command alone is not enough to trigger a notification alerting
|
||
you of the queue's overwhelmed status. When the command encounters a queue
|
||
that has a job count exceeding your threshold, an
|
||
`Illuminate\Queue\Events\QueueBusy` event will be dispatched. You may listen
|
||
for this event within your application's `AppServiceProvider` in order to send
|
||
a notification to you or your development team:
|
||
|
||
|
||
|
||
1use App\Notifications\QueueHasLongWaitTime;
|
||
|
||
2use Illuminate\Queue\Events\QueueBusy;
|
||
|
||
3use Illuminate\Support\Facades\Event;
|
||
|
||
4use Illuminate\Support\Facades\Notification;
|
||
|
||
5
|
||
|
||
6/**
|
||
|
||
7 * Bootstrap any application services.
|
||
|
||
8 */
|
||
|
||
9public function boot(): void
|
||
|
||
10{
|
||
|
||
11 Event::listen(function (QueueBusy $event) {
|
||
|
||
12 Notification::route('mail', '[[email protected]](/cdn-cgi/l/email-protection)')
|
||
|
||
13 ->notify(new QueueHasLongWaitTime(
|
||
|
||
14 $event->connection,
|
||
|
||
15 $event->queue,
|
||
|
||
16 $event->size
|
||
|
||
17 ));
|
||
|
||
18 });
|
||
|
||
19}
|
||
|
||
|
||
use App\Notifications\QueueHasLongWaitTime;
|
||
use Illuminate\Queue\Events\QueueBusy;
|
||
use Illuminate\Support\Facades\Event;
|
||
use Illuminate\Support\Facades\Notification;
|
||
|
||
/**
|
||
* Bootstrap any application services.
|
||
*/
|
||
public function boot(): void
|
||
{
|
||
Event::listen(function (QueueBusy $event) {
|
||
Notification::route('mail', '[[email protected]](/cdn-cgi/l/email-protection)')
|
||
->notify(new QueueHasLongWaitTime(
|
||
$event->connection,
|
||
$event->queue,
|
||
$event->size
|
||
));
|
||
});
|
||
}
|
||
|
||
## Testing
|
||
|
||
When testing code that dispatches jobs, you may wish to instruct Laravel to
|
||
not actually execute the job itself, since the job's code can be tested
|
||
directly and separately of the code that dispatches it. Of course, to test the
|
||
job itself, you may instantiate a job instance and invoke the `handle` method
|
||
directly in your test.
|
||
|
||
You may use the `Queue` facade's `fake` method to prevent queued jobs from
|
||
actually being pushed to the queue. After calling the `Queue` facade's `fake`
|
||
method, you may then assert that the application attempted to push jobs to the
|
||
queue:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3use App\Jobs\AnotherJob;
|
||
|
||
4use App\Jobs\ShipOrder;
|
||
|
||
5use Illuminate\Support\Facades\Queue;
|
||
|
||
6
|
||
|
||
7test('orders can be shipped', function () {
|
||
|
||
8 Queue::fake();
|
||
|
||
9
|
||
|
||
10 // Perform order shipping...
|
||
|
||
11
|
||
|
||
12 // Assert that no jobs were pushed...
|
||
|
||
13 Queue::assertNothingPushed();
|
||
|
||
14
|
||
|
||
15 // Assert a job was pushed to a given queue...
|
||
|
||
16 Queue::assertPushedOn('queue-name', ShipOrder::class);
|
||
|
||
17
|
||
|
||
18 // Assert a job was pushed twice...
|
||
|
||
19 Queue::assertPushed(ShipOrder::class, 2);
|
||
|
||
20
|
||
|
||
21 // Assert a job was not pushed...
|
||
|
||
22 Queue::assertNotPushed(AnotherJob::class);
|
||
|
||
23
|
||
|
||
24 // Assert that a closure was pushed to the queue...
|
||
|
||
25 Queue::assertClosurePushed();
|
||
|
||
26
|
||
|
||
27 // Assert that a closure was not pushed...
|
||
|
||
28 Queue::assertClosureNotPushed();
|
||
|
||
29
|
||
|
||
30 // Assert the total number of jobs that were pushed...
|
||
|
||
31 Queue::assertCount(3);
|
||
|
||
32});
|
||
|
||
|
||
<?php
|
||
|
||
use App\Jobs\AnotherJob;
|
||
use App\Jobs\ShipOrder;
|
||
use Illuminate\Support\Facades\Queue;
|
||
|
||
test('orders can be shipped', function () {
|
||
Queue::fake();
|
||
|
||
// Perform order shipping...
|
||
|
||
// Assert that no jobs were pushed...
|
||
Queue::assertNothingPushed();
|
||
|
||
// Assert a job was pushed to a given queue...
|
||
Queue::assertPushedOn('queue-name', ShipOrder::class);
|
||
|
||
// Assert a job was pushed twice...
|
||
Queue::assertPushed(ShipOrder::class, 2);
|
||
|
||
// Assert a job was not pushed...
|
||
Queue::assertNotPushed(AnotherJob::class);
|
||
|
||
// Assert that a closure was pushed to the queue...
|
||
Queue::assertClosurePushed();
|
||
|
||
// Assert that a closure was not pushed...
|
||
Queue::assertClosureNotPushed();
|
||
|
||
// Assert the total number of jobs that were pushed...
|
||
Queue::assertCount(3);
|
||
});
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace Tests\Feature;
|
||
|
||
4
|
||
|
||
5use App\Jobs\AnotherJob;
|
||
|
||
6use App\Jobs\ShipOrder;
|
||
|
||
7use Illuminate\Support\Facades\Queue;
|
||
|
||
8use Tests\TestCase;
|
||
|
||
9
|
||
|
||
10class ExampleTest extends TestCase
|
||
|
||
11{
|
||
|
||
12 public function test_orders_can_be_shipped(): void
|
||
|
||
13 {
|
||
|
||
14 Queue::fake();
|
||
|
||
15
|
||
|
||
16 // Perform order shipping...
|
||
|
||
17
|
||
|
||
18 // Assert that no jobs were pushed...
|
||
|
||
19 Queue::assertNothingPushed();
|
||
|
||
20
|
||
|
||
21 // Assert a job was pushed to a given queue...
|
||
|
||
22 Queue::assertPushedOn('queue-name', ShipOrder::class);
|
||
|
||
23
|
||
|
||
24 // Assert a job was pushed twice...
|
||
|
||
25 Queue::assertPushed(ShipOrder::class, 2);
|
||
|
||
26
|
||
|
||
27 // Assert a job was not pushed...
|
||
|
||
28 Queue::assertNotPushed(AnotherJob::class);
|
||
|
||
29
|
||
|
||
30 // Assert that a closure was pushed to the queue...
|
||
|
||
31 Queue::assertClosurePushed();
|
||
|
||
32
|
||
|
||
33 // Assert that a closure was not pushed...
|
||
|
||
34 Queue::assertClosureNotPushed();
|
||
|
||
35
|
||
|
||
36 // Assert the total number of jobs that were pushed...
|
||
|
||
37 Queue::assertCount(3);
|
||
|
||
38 }
|
||
|
||
39}
|
||
|
||
|
||
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Jobs\AnotherJob;
|
||
use App\Jobs\ShipOrder;
|
||
use Illuminate\Support\Facades\Queue;
|
||
use Tests\TestCase;
|
||
|
||
class ExampleTest extends TestCase
|
||
{
|
||
public function test_orders_can_be_shipped(): void
|
||
{
|
||
Queue::fake();
|
||
|
||
// Perform order shipping...
|
||
|
||
// Assert that no jobs were pushed...
|
||
Queue::assertNothingPushed();
|
||
|
||
// Assert a job was pushed to a given queue...
|
||
Queue::assertPushedOn('queue-name', ShipOrder::class);
|
||
|
||
// Assert a job was pushed twice...
|
||
Queue::assertPushed(ShipOrder::class, 2);
|
||
|
||
// Assert a job was not pushed...
|
||
Queue::assertNotPushed(AnotherJob::class);
|
||
|
||
// Assert that a closure was pushed to the queue...
|
||
Queue::assertClosurePushed();
|
||
|
||
// Assert that a closure was not pushed...
|
||
Queue::assertClosureNotPushed();
|
||
|
||
// Assert the total number of jobs that were pushed...
|
||
Queue::assertCount(3);
|
||
}
|
||
}
|
||
|
||
You may pass a closure to the `assertPushed`, `assertNotPushed`,
|
||
`assertClosurePushed`, or `assertClosureNotPushed` methods in order to assert
|
||
that a job was pushed that passes a given "truth test". If at least one job
|
||
was pushed that passes the given truth test then the assertion will be
|
||
successful:
|
||
|
||
|
||
|
||
1use Illuminate\Queue\CallQueuedClosure;
|
||
|
||
2
|
||
|
||
3Queue::assertPushed(function (ShipOrder $job) use ($order) {
|
||
|
||
4 return $job->order->id === $order->id;
|
||
|
||
5});
|
||
|
||
6
|
||
|
||
7Queue::assertClosurePushed(function (CallQueuedClosure $job) {
|
||
|
||
8 return $job->name === 'validate-order';
|
||
|
||
9});
|
||
|
||
|
||
use Illuminate\Queue\CallQueuedClosure;
|
||
|
||
Queue::assertPushed(function (ShipOrder $job) use ($order) {
|
||
return $job->order->id === $order->id;
|
||
});
|
||
|
||
Queue::assertClosurePushed(function (CallQueuedClosure $job) {
|
||
return $job->name === 'validate-order';
|
||
});
|
||
|
||
### Faking a Subset of Jobs
|
||
|
||
If you only need to fake specific jobs while allowing your other jobs to
|
||
execute normally, you may pass the class names of the jobs that should be
|
||
faked to the `fake` method:
|
||
|
||
Pest PHPUnit
|
||
|
||
|
||
|
||
1test('orders can be shipped', function () {
|
||
|
||
2 Queue::fake([
|
||
|
||
3 ShipOrder::class,
|
||
|
||
4 ]);
|
||
|
||
5
|
||
|
||
6 // Perform order shipping...
|
||
|
||
7
|
||
|
||
8 // Assert a job was pushed twice...
|
||
|
||
9 Queue::assertPushed(ShipOrder::class, 2);
|
||
|
||
10});
|
||
|
||
|
||
test('orders can be shipped', function () {
|
||
Queue::fake([
|
||
ShipOrder::class,
|
||
]);
|
||
|
||
// Perform order shipping...
|
||
|
||
// Assert a job was pushed twice...
|
||
Queue::assertPushed(ShipOrder::class, 2);
|
||
});
|
||
|
||
|
||
1public function test_orders_can_be_shipped(): void
|
||
|
||
2{
|
||
|
||
3 Queue::fake([
|
||
|
||
4 ShipOrder::class,
|
||
|
||
5 ]);
|
||
|
||
6
|
||
|
||
7 // Perform order shipping...
|
||
|
||
8
|
||
|
||
9 // Assert a job was pushed twice...
|
||
|
||
10 Queue::assertPushed(ShipOrder::class, 2);
|
||
|
||
11}
|
||
|
||
|
||
public function test_orders_can_be_shipped(): void
|
||
{
|
||
Queue::fake([
|
||
ShipOrder::class,
|
||
]);
|
||
|
||
// Perform order shipping...
|
||
|
||
// Assert a job was pushed twice...
|
||
Queue::assertPushed(ShipOrder::class, 2);
|
||
}
|
||
|
||
You may fake all jobs except for a set of specified jobs using the `except`
|
||
method:
|
||
|
||
|
||
|
||
1Queue::fake()->except([
|
||
|
||
2 ShipOrder::class,
|
||
|
||
3]);
|
||
|
||
|
||
Queue::fake()->except([
|
||
ShipOrder::class,
|
||
]);
|
||
|
||
### Testing Job Chains
|
||
|
||
To test job chains, you will need to utilize the `Bus` facade's faking
|
||
capabilities. The `Bus` facade's `assertChained` method may be used to assert
|
||
that a [chain of jobs](/docs/12.x/queues#job-chaining) was dispatched. The
|
||
`assertChained` method accepts an array of chained jobs as its first argument:
|
||
|
||
|
||
|
||
1use App\Jobs\RecordShipment;
|
||
|
||
2use App\Jobs\ShipOrder;
|
||
|
||
3use App\Jobs\UpdateInventory;
|
||
|
||
4use Illuminate\Support\Facades\Bus;
|
||
|
||
5
|
||
|
||
6Bus::fake();
|
||
|
||
7
|
||
|
||
8// ...
|
||
|
||
9
|
||
|
||
10Bus::assertChained([
|
||
|
||
11 ShipOrder::class,
|
||
|
||
12 RecordShipment::class,
|
||
|
||
13 UpdateInventory::class
|
||
|
||
14]);
|
||
|
||
|
||
use App\Jobs\RecordShipment;
|
||
use App\Jobs\ShipOrder;
|
||
use App\Jobs\UpdateInventory;
|
||
use Illuminate\Support\Facades\Bus;
|
||
|
||
Bus::fake();
|
||
|
||
// ...
|
||
|
||
Bus::assertChained([
|
||
ShipOrder::class,
|
||
RecordShipment::class,
|
||
UpdateInventory::class
|
||
]);
|
||
|
||
As you can see in the example above, the array of chained jobs may be an array
|
||
of the job's class names. However, you may also provide an array of actual job
|
||
instances. When doing so, Laravel will ensure that the job instances are of
|
||
the same class and have the same property values of the chained jobs
|
||
dispatched by your application:
|
||
|
||
|
||
|
||
1Bus::assertChained([
|
||
|
||
2 new ShipOrder,
|
||
|
||
3 new RecordShipment,
|
||
|
||
4 new UpdateInventory,
|
||
|
||
5]);
|
||
|
||
|
||
Bus::assertChained([
|
||
new ShipOrder,
|
||
new RecordShipment,
|
||
new UpdateInventory,
|
||
]);
|
||
|
||
You may use the `assertDispatchedWithoutChain` method to assert that a job was
|
||
pushed without a chain of jobs:
|
||
|
||
|
||
|
||
1Bus::assertDispatchedWithoutChain(ShipOrder::class);
|
||
|
||
|
||
Bus::assertDispatchedWithoutChain(ShipOrder::class);
|
||
|
||
#### Testing Chain Modifications
|
||
|
||
If a chained job prepends or appends jobs to an existing chain, you may use
|
||
the job's `assertHasChain` method to assert that the job has the expected
|
||
chain of remaining jobs:
|
||
|
||
|
||
|
||
1$job = new ProcessPodcast;
|
||
|
||
2
|
||
|
||
3$job->handle();
|
||
|
||
4
|
||
|
||
5$job->assertHasChain([
|
||
|
||
6 new TranscribePodcast,
|
||
|
||
7 new OptimizePodcast,
|
||
|
||
8 new ReleasePodcast,
|
||
|
||
9]);
|
||
|
||
|
||
$job = new ProcessPodcast;
|
||
|
||
$job->handle();
|
||
|
||
$job->assertHasChain([
|
||
new TranscribePodcast,
|
||
new OptimizePodcast,
|
||
new ReleasePodcast,
|
||
]);
|
||
|
||
The `assertDoesntHaveChain` method may be used to assert that the job's
|
||
remaining chain is empty:
|
||
|
||
|
||
|
||
1$job->assertDoesntHaveChain();
|
||
|
||
|
||
$job->assertDoesntHaveChain();
|
||
|
||
#### Testing Chained Batches
|
||
|
||
If your job chain contains a batch of jobs, you may assert that the chained
|
||
batch matches your expectations by inserting a `Bus::chainedBatch` definition
|
||
within your chain assertion:
|
||
|
||
|
||
|
||
1use App\Jobs\ShipOrder;
|
||
|
||
2use App\Jobs\UpdateInventory;
|
||
|
||
3use Illuminate\Bus\PendingBatch;
|
||
|
||
4use Illuminate\Support\Facades\Bus;
|
||
|
||
5
|
||
|
||
6Bus::assertChained([
|
||
|
||
7 new ShipOrder,
|
||
|
||
8 Bus::chainedBatch(function (PendingBatch $batch) {
|
||
|
||
9 return $batch->jobs->count() === 3;
|
||
|
||
10 }),
|
||
|
||
11 new UpdateInventory,
|
||
|
||
12]);
|
||
|
||
|
||
use App\Jobs\ShipOrder;
|
||
use App\Jobs\UpdateInventory;
|
||
use Illuminate\Bus\PendingBatch;
|
||
use Illuminate\Support\Facades\Bus;
|
||
|
||
Bus::assertChained([
|
||
new ShipOrder,
|
||
Bus::chainedBatch(function (PendingBatch $batch) {
|
||
return $batch->jobs->count() === 3;
|
||
}),
|
||
new UpdateInventory,
|
||
]);
|
||
|
||
### Testing Job Batches
|
||
|
||
The `Bus` facade's `assertBatched` method may be used to assert that a [batch
|
||
of jobs](/docs/12.x/queues#job-batching) was dispatched. The closure given to
|
||
the `assertBatched` method receives an instance of
|
||
`Illuminate\Bus\PendingBatch`, which may be used to inspect the jobs within
|
||
the batch:
|
||
|
||
|
||
|
||
1use Illuminate\Bus\PendingBatch;
|
||
|
||
2use Illuminate\Support\Facades\Bus;
|
||
|
||
3
|
||
|
||
4Bus::fake();
|
||
|
||
5
|
||
|
||
6// ...
|
||
|
||
7
|
||
|
||
8Bus::assertBatched(function (PendingBatch $batch) {
|
||
|
||
9 return $batch->name == 'Import CSV' &&
|
||
|
||
10 $batch->jobs->count() === 10;
|
||
|
||
11});
|
||
|
||
|
||
use Illuminate\Bus\PendingBatch;
|
||
use Illuminate\Support\Facades\Bus;
|
||
|
||
Bus::fake();
|
||
|
||
// ...
|
||
|
||
Bus::assertBatched(function (PendingBatch $batch) {
|
||
return $batch->name == 'Import CSV' &&
|
||
$batch->jobs->count() === 10;
|
||
});
|
||
|
||
You may use the `assertBatchCount` method to assert that a given number of
|
||
batches were dispatched:
|
||
|
||
|
||
|
||
1Bus::assertBatchCount(3);
|
||
|
||
|
||
Bus::assertBatchCount(3);
|
||
|
||
You may use `assertNothingBatched` to assert that no batches were dispatched:
|
||
|
||
|
||
|
||
1Bus::assertNothingBatched();
|
||
|
||
|
||
Bus::assertNothingBatched();
|
||
|
||
#### Testing Job / Batch Interaction
|
||
|
||
In addition, you may occasionally need to test an individual job's interaction
|
||
with its underlying batch. For example, you may need to test if a job
|
||
cancelled further processing for its batch. To accomplish this, you need to
|
||
assign a fake batch to the job via the `withFakeBatch` method. The
|
||
`withFakeBatch` method returns a tuple containing the job instance and the
|
||
fake batch:
|
||
|
||
|
||
|
||
1[$job, $batch] = (new ShipOrder)->withFakeBatch();
|
||
|
||
2
|
||
|
||
3$job->handle();
|
||
|
||
4
|
||
|
||
5$this->assertTrue($batch->cancelled());
|
||
|
||
6$this->assertEmpty($batch->added);
|
||
|
||
|
||
[$job, $batch] = (new ShipOrder)->withFakeBatch();
|
||
|
||
$job->handle();
|
||
|
||
$this->assertTrue($batch->cancelled());
|
||
$this->assertEmpty($batch->added);
|
||
|
||
### Testing Job / Queue Interactions
|
||
|
||
Sometimes, you may need to test that a queued job releases itself back onto
|
||
the queue. Or, you may need to test that the job deleted itself. You may test
|
||
these queue interactions by instantiating the job and invoking the
|
||
`withFakeQueueInteractions` method.
|
||
|
||
Once the job's queue interactions have been faked, you may invoke the `handle`
|
||
method on the job. After invoking the job, various assertion methods are
|
||
available to verify the job's queue interactions:
|
||
|
||
|
||
|
||
1use App\Exceptions\CorruptedAudioException;
|
||
|
||
2use App\Jobs\ProcessPodcast;
|
||
|
||
3
|
||
|
||
4$job = (new ProcessPodcast)->withFakeQueueInteractions();
|
||
|
||
5
|
||
|
||
6$job->handle();
|
||
|
||
7
|
||
|
||
8$job->assertReleased(delay: 30);
|
||
|
||
9$job->assertDeleted();
|
||
|
||
10$job->assertNotDeleted();
|
||
|
||
11$job->assertFailed();
|
||
|
||
12$job->assertFailedWith(CorruptedAudioException::class);
|
||
|
||
13$job->assertNotFailed();
|
||
|
||
|
||
use App\Exceptions\CorruptedAudioException;
|
||
use App\Jobs\ProcessPodcast;
|
||
|
||
$job = (new ProcessPodcast)->withFakeQueueInteractions();
|
||
|
||
$job->handle();
|
||
|
||
$job->assertReleased(delay: 30);
|
||
$job->assertDeleted();
|
||
$job->assertNotDeleted();
|
||
$job->assertFailed();
|
||
$job->assertFailedWith(CorruptedAudioException::class);
|
||
$job->assertNotFailed();
|
||
|
||
## Job Events
|
||
|
||
Using the `before` and `after` methods on the `Queue`
|
||
[facade](/docs/12.x/facades), you may specify callbacks to be executed before
|
||
or after a queued job is processed. These callbacks are a great opportunity to
|
||
perform additional logging or increment statistics for a dashboard. Typically,
|
||
you should call these methods from the `boot` method of a [service
|
||
provider](/docs/12.x/providers). For example, we may use the
|
||
`AppServiceProvider` that is included with Laravel:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Providers;
|
||
|
||
4
|
||
|
||
5use Illuminate\Support\Facades\Queue;
|
||
|
||
6use Illuminate\Support\ServiceProvider;
|
||
|
||
7use Illuminate\Queue\Events\JobProcessed;
|
||
|
||
8use Illuminate\Queue\Events\JobProcessing;
|
||
|
||
9
|
||
|
||
10class AppServiceProvider extends ServiceProvider
|
||
|
||
11{
|
||
|
||
12 /**
|
||
|
||
13 * Register any application services.
|
||
|
||
14 */
|
||
|
||
15 public function register(): void
|
||
|
||
16 {
|
||
|
||
17 // ...
|
||
|
||
18 }
|
||
|
||
19
|
||
|
||
20 /**
|
||
|
||
21 * Bootstrap any application services.
|
||
|
||
22 */
|
||
|
||
23 public function boot(): void
|
||
|
||
24 {
|
||
|
||
25 Queue::before(function (JobProcessing $event) {
|
||
|
||
26 // $event->connectionName
|
||
|
||
27 // $event->job
|
||
|
||
28 // $event->job->payload()
|
||
|
||
29 });
|
||
|
||
30
|
||
|
||
31 Queue::after(function (JobProcessed $event) {
|
||
|
||
32 // $event->connectionName
|
||
|
||
33 // $event->job
|
||
|
||
34 // $event->job->payload()
|
||
|
||
35 });
|
||
|
||
36 }
|
||
|
||
37}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Providers;
|
||
|
||
use Illuminate\Support\Facades\Queue;
|
||
use Illuminate\Support\ServiceProvider;
|
||
use Illuminate\Queue\Events\JobProcessed;
|
||
use Illuminate\Queue\Events\JobProcessing;
|
||
|
||
class AppServiceProvider extends ServiceProvider
|
||
{
|
||
/**
|
||
* Register any application services.
|
||
*/
|
||
public function register(): void
|
||
{
|
||
// ...
|
||
}
|
||
|
||
/**
|
||
* Bootstrap any application services.
|
||
*/
|
||
public function boot(): void
|
||
{
|
||
Queue::before(function (JobProcessing $event) {
|
||
// $event->connectionName
|
||
// $event->job
|
||
// $event->job->payload()
|
||
});
|
||
|
||
Queue::after(function (JobProcessed $event) {
|
||
// $event->connectionName
|
||
// $event->job
|
||
// $event->job->payload()
|
||
});
|
||
}
|
||
}
|
||
|
||
Using the `looping` method on the `Queue` [facade](/docs/12.x/facades), you
|
||
may specify callbacks that execute before the worker attempts to fetch a job
|
||
from a queue. For example, you might register a closure to rollback any
|
||
transactions that were left open by a previously failed job:
|
||
|
||
|
||
|
||
1use Illuminate\Support\Facades\DB;
|
||
|
||
2use Illuminate\Support\Facades\Queue;
|
||
|
||
3
|
||
|
||
4Queue::looping(function () {
|
||
|
||
5 while (DB::transactionLevel() > 0) {
|
||
|
||
6 DB::rollBack();
|
||
|
||
7 }
|
||
|
||
8});
|
||
|
||
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Queue;
|
||
|
||
Queue::looping(function () {
|
||
while (DB::transactionLevel() > 0) {
|
||
DB::rollBack();
|
||
}
|
||
});
|
||
|