Skip to content

Commit 84bddb2

Browse files
committed
Batched job improvements
1 parent 00685a8 commit 84bddb2

File tree

1 file changed

+35
-22
lines changed

1 file changed

+35
-22
lines changed

docs/5.x/extend/queue-jobs.md

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This feature relies on [Yii’s queue system](https://www.yiiframework.com/exten
1111
- The ability to set a fallback description for the job.
1212
- The ability to label and report task progress for a better user experience.
1313

14-
## To Queue or Not to Queue
14+
## To Queue or Not to Queue?
1515

1616
An ideal queue job is something slow that the a user shouldn’t have to wait on while using a site’s front end or the control panel. Multi-step processes and actions that connect with third-party APIs can be ideal candidates for queueing.
1717

@@ -40,15 +40,15 @@ class MyJob extends \craft\queue\BaseJob
4040
$message = new Message();
4141

4242
$message->setTo('to@domain.tld');
43-
$message->setSubject('Oh Hai');
44-
$message->setTextBody('Hello from the queue system! 👋');
43+
$message->setSubject('Craft Queue Test');
44+
$message->setTextBody('Hello from the Craft queue worker! 👋');
4545

4646
Craft::$app->getMailer()->send($message);
4747
}
4848

4949
protected function defaultDescription(): string
5050
{
51-
return Craft::t('app', 'Sending a worthless email');
51+
return Craft::t('app', 'Sending a test email');
5252
}
5353
}
5454
```
@@ -63,16 +63,14 @@ You can do this with BaseJob’s [`setProgress()`](craft5:craft\queue\BaseJob::s
6363
- a number between 0 and 1 representing the percent complete
6464
- an optional, human-friendly label describing the progress
6565

66-
We can modify our example `execute()` method to send an email to every Craft user and report the job’s progress.
67-
68-
::: danger
69-
Do not lightheartedly send an email to every Craft user. Not cool.
70-
:::
66+
We can modify our example `execute()` method to send an email to a number of users and report the job’s progress between each send.
7167

7268
```php{7-14}
7369
public function execute($queue): void
7470
{
75-
$users = \craft\elements\User::findAll();
71+
$users = \craft\elements\User::find()
72+
->admin(false)
73+
->all();
7674
$totalUsers = count($users);
7775
7876
foreach ($users as $i => $user) {
@@ -88,8 +86,8 @@ public function execute($queue): void
8886
$message = new Message();
8987
9088
$message->setTo($user->email);
91-
$message->setSubject('Oh Hai');
92-
$message->setTextBody('Hello from the queue system! 👋');
89+
$message->setSubject('Craft Queue Test');
90+
$message->setTextBody('Hello from the Craft queue worker! 👋');
9391
9492
// Swallow exceptions from the mailer:
9593
try {
@@ -101,29 +99,31 @@ public function execute($queue): void
10199
}
102100
```
103101

102+
Depending on the number of users (or any kind of record or item) and the app’s mailer configuration, this may take longer than the queue’s allowed <abbr title="Time to reserve">TTR</abbr>. Consider [batching](#batched-jobs) jobs that act on many individual items!
103+
104104
### Dealing with Failed Jobs
105105

106106
In our first example, exceptions from the mailer can bubble out of our job—but in the second example, we catch those errors so the job is not halted prematurely.
107107

108-
This decision is up to you: if the work in a job is nonessential (or will be done again later, like <craft5:craft\queue\jobs\GeneratePendingTransforms>), you can catch and log errors and let the job end nominally; if the work is critical (like synchronizing something to an external API), it may be better to let the exception bubble out of `execute()`.
108+
This decision is up to you: if the work in a job is nonessential (or can be done again later without consequence, as is the case with <craft5:craft\queue\jobs\GeneratePendingTransforms>), you can catch errors, log them, and let the job exit nominally; if the work is critical (like synchronizing something to or from an external API), it may be better to let the exception bubble out of `execute()`.
109109

110110
The queue wraps every job in its own `try` block, and will mark any jobs that throw exceptions as _failed_. The exception message that caused the failure will be recorded along with the job. Failed jobs can be retried from the control panel or with the `php craft queue/retry [id]` command.
111111

112112
#### Retryable Jobs
113113

114-
The queue will automatically retry failed jobs that implement the [`RetryableJobInterface`](https://www.yiiframework.com/extension/yiisoft/yii2-queue/doc/guide/2.0/en/retryable#retryablejobinterface). A job will only be retried after its `ttr` has passed—even if it didn’t use up the allowed time, and will be marked as failed once `canRetry()` returns false.
114+
The queue will automatically retry failed jobs that implement [`RetryableJobInterface`](https://www.yiiframework.com/extension/yiisoft/yii2-queue/doc/guide/2.0/en/retryable#retryablejobinterface). A job will only be retried after its “time to reserve” has passed—even if it didn’t use up the allowed timeand will be marked as failed once `canRetry()` returns false.
115115

116116
::: warning
117-
Returning `true` from `canRetry()` can pollute your queue with jobs that may never succeed. Failed jobs are not necessarily bad! Exceptions can be used to track failures in code that runs unattended.
117+
Unconditionally returning `true` from `canRetry()` will pollute your queue with jobs that may never succeed. Failed jobs are not necessarily bad—exceptions can (and should) be thrown to track failures in code that runs unattended!
118118
:::
119119

120120
### Batched Jobs
121121

122122
In situations where there is simply too much work to do in a single request (or within PHP’s memory limit), consider extending <craft5:craft\queue\BaseBatchedJob>.
123123

124-
Batched jobs’ `execute()` method is handled for you. Instead, you must define two new methods:
124+
Batched jobs’ `execute()` method is implemented for you. Instead, you must define two new methods:
125125

126-
- `loadData()` — Returns a class implementing <craft5:craft\base\Batchable>, like <craft5:craft\db\QueryBatcher>. Data is not necessarily loaded at this point, but a means of fetching data in “slices” must be.
126+
- `loadData()` — Returns a class implementing <craft5:craft\base\Batchable>, like <craft5:craft\db\QueryBatcher>. Data is not necessarily _loaded_ at this point—instead, you’ll return a means of fetching data in “slices” or “pages.”
127127
- `processItem($item)` — Your logic for handling a single item in each batch.
128128

129129
::: tip
@@ -143,23 +143,36 @@ return new QueryBatcher($query);
143143
Also note that we’re explicitly ordering by `id`—this can help avoid skipped or double-processed items across batches when the underlying data changes (including changes made _within a job_)!
144144
:::
145145

146-
Batched jobs can also define a default `$batchSize` that is appropriate for the workload. The batch size is not a guaranteed value, but a target when Craft spawns the next job—it will keep track of memory usage and _may_ stop short, scheduling the next batch to resume where it left off.
146+
Batched jobs can also define a default `$batchSize` that is appropriate for the workload. The batch size is a _maximum_ value, carried from job to job—Craft keeps track of memory usage and _may_ stop short and push a new job for the next batch. Every job maintains an `itemOffset` so they can be resumed exactly where the last one left off.
147147

148148
::: warning
149149
Batched jobs **must** be pushed using <craft5:craft\helpers\Queue::push()>, or `delay` and `ttr` settings may be lost for subsequent batches.
150150
:::
151151

152+
#### Batch Hooks
153+
154+
Special setup and cleanup actions can be defined in two pairs of methods:
155+
156+
| Method… | Runs… |
157+
| `before()` | …at the very beginning of a series of batched jobs. |
158+
| `beforeBatch()` | …prior to each batch in a series. |
159+
| `afterBatch()` | …after each batch in a series. |
160+
| `after()` | …at the very end of a series of batched jobs. |
161+
162+
`before()` and `after()` are invoked _once_ per manually-pushed batched job, whereas `beforeBatch()` and `afterBatch()` are run for _every_ slice. If all the items in a batched job are processed in a single job, it will invoke all four hooks.
163+
152164
## Adding Your Job to the Queue
153165

154-
Once you’ve created your job, you can add it to the queue:
166+
Once you’ve created your job, add it to the queue with the helper:
155167

156-
```php
168+
```php{4}
169+
use craft\helpers\Queue;
157170
use mynamespace\queue\jobs\MyJob;
158171
159-
\craft\helpers\Queue::push(new MyJob());
172+
Queue::push(new MyJob());
160173
```
161174

162-
You can do this wherever it makes sense—most likely in a controller action or service.
175+
Users and developers typically cannot (and should not need to) trigger specific jobs at-will, so it’s important that your plugin push jobs where it makes sense—typically in a [controller action](controllers.md) or [service](services.md).
163176

164177
### Specifying Priority
165178

0 commit comments

Comments
 (0)