|
1 | 1 | # Error handling on message processing |
2 | 2 |
|
3 | | -Often when some message handling is failing, we want to retry its execution a couple more times or redirect it to another queue channel. This can be done in [yiisoft/queue](https://github.com/yiisoft/queue) with _Failure Handling Middleware Pipeline_. It is triggered each time message processing via Consume Middleware Pipeline is interrupted with any `Throwable`. |
| 3 | +Often when some message handling is failing, we want to retry its execution a couple more times or redirect it to another queue channel. In `yiisoft/queue` this is handled by the failure handling middleware pipeline. |
| 4 | + |
| 5 | +## When failure handling is triggered |
| 6 | + |
| 7 | +Failure handling is triggered only when message processing throws a `Throwable`. |
| 8 | + |
| 9 | +In practice it means: |
| 10 | + |
| 11 | +- The worker runs message processing in `Yiisoft\Queue\Worker\Worker::process()`. |
| 12 | +- Your message handler is executed through the [consume middleware pipeline](middleware-pipelines.md#consume-pipeline). |
| 13 | +- If any `Throwable` escapes that pipeline, the worker switches to the failure handling pipeline. |
| 14 | + |
| 15 | +## Failure handling pipeline overview (step-by-step) |
| 16 | + |
| 17 | +1. A message is processed via the consume pipeline |
| 18 | + |
| 19 | + The worker builds a `Yiisoft\Queue\Middleware\Consume\ConsumeRequest` and dispatches it through `ConsumeMiddlewareDispatcher`. The final consume handler invokes the resolved message handler. |
| 20 | + |
| 21 | +2. A `Throwable` is caught by the worker |
| 22 | + |
| 23 | + If any middleware or the message handler throws, `Worker::process()` catches it. |
| 24 | + |
| 25 | +3. Failure context is wrapped into a request object |
| 26 | + |
| 27 | + The worker creates a `Yiisoft\Queue\Middleware\FailureHandling\FailureHandlingRequest` ([source](../../../src/Middleware/FailureHandling/FailureHandlingRequest.php)) containing: |
| 28 | + |
| 29 | + - the message |
| 30 | + - the caught exception |
| 31 | + - the queue instance (including its channel) |
| 32 | + |
| 33 | +4. A failure pipeline is selected by queue channel |
| 34 | + |
| 35 | + `FailureMiddlewareDispatcher::dispatch()` selects which pipeline to run: |
| 36 | + |
| 37 | + - It tries to use the pipeline configured for the current queue channel. |
| 38 | + - If there is no pipeline for that channel (or it is empty), it falls back to `FailureMiddlewareDispatcher::DEFAULT_PIPELINE`. |
| 39 | + |
| 40 | +5. Failure middlewares are executed |
| 41 | + |
| 42 | + The dispatcher builds a lazy middleware stack (`MiddlewareFailureStack`) and invokes it. |
| 43 | + |
| 44 | + Each failure middleware implements `MiddlewareFailureInterface`: |
| 45 | + |
| 46 | + - It receives the `FailureHandlingRequest` and a continuation handler. |
| 47 | + - It may handle the failure by re-queueing the message (same or different queue/channel), optionally with a delay. |
| 48 | + - If it decides not to handle the failure, it calls `$handler->handleFailure($request)` to continue the pipeline. |
| 49 | + |
| 50 | +6. If nothing handles the failure, the exception is rethrown |
| 51 | + |
| 52 | + The failure pipeline ends with `FailureFinalHandler`, which throws `$request->getException()`. |
| 53 | + |
| 54 | +7. The worker wraps and rethrows |
| 55 | + |
| 56 | + If the failure pipeline itself ends with an exception, `Worker::process()` wraps it into `Yiisoft\Queue\Exception\JobFailureException` (including message id from `IdEnvelope` metadata when available) and throws it. |
| 57 | + |
| 58 | +## What “handled failure” means |
| 59 | + |
| 60 | +A failure is considered handled if the failure pipeline returns a `FailureHandlingRequest` without throwing. |
| 61 | +In practice, built-in middlewares handle failures by re-queueing the message (same or different queue/channel), optionally with a delay, and returning the updated request. |
4 | 62 |
|
5 | 63 | ## Configuration |
6 | 64 |
|
7 | | -Here below is configuration via [yiisoft/config](https://github.com/yiisoft/config). If you don't use it, you should add a middleware definition list (in the `middlewares-fail` key here) to the `FailureMiddlewareDispatcher` by your own. You can define different failure handling pipelines for each queue channel. The example below defines two different failure handling pipelines: |
| 65 | +Here below is configuration via [yiisoft/config](https://github.com/yiisoft/config) (see also [Configuration with yiisoft/config](configuration-with-config.md)). If you don't use it, you should add a middleware definition list (in the `middlewares-fail` key here) to the `FailureMiddlewareDispatcher` [by your own](configuration-manual.md). You can define different failure handling pipelines for each queue channel (see [Queue channels](channels.md)). The example below defines two different failure handling pipelines: |
8 | 66 |
|
9 | 67 | ```php |
10 | 68 | 'yiisoft/queue' => [ |
@@ -57,29 +115,77 @@ In the example above failures will be handled this way (look the concrete middle |
57 | 115 | Failures of messages, which are initially sent to the `failed-messages` channel, will only be handled by the 3rd and the 4th points of this list. |
58 | 116 |
|
59 | 117 | ## Default failure handling strategies |
| 118 | + |
| 119 | + Let's see the built-in defaults. |
| 120 | + |
| 121 | + ### [SendAgainMiddleware](../../../src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php) |
| 122 | + |
| 123 | + This strategy simply resends the given message to a queue. Let's see the constructor parameters through which it's configured: |
| 124 | + |
| 125 | + - `id` - A unique string. Allows to use this strategy more than once for the same message, just like in example above. |
| 126 | + - `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. |
| 127 | + - `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue channel it came from. When the `queue` parameter is set to `null`, a message will be sent to the same channel it came from. |
| 128 | + |
| 129 | + State tracking: |
| 130 | + |
| 131 | + - Uses `FailureEnvelope` metadata (`failure-meta`) to store the per-middleware attempt counter. |
| 132 | + - The counter key is `failure-strategy-resend-attempts-{id}`. |
| 133 | + |
| 134 | + ### [ExponentialDelayMiddleware](../../../src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php) |
| 135 | + |
| 136 | + This strategy does the same thing as the `SendAgainMiddleware` with a single difference: it resends a message with an exponentially increasing delay. The delay **must** be implemented by the used `AdapterInterface` implementation. |
60 | 137 |
|
61 | | -Let's see the built-in defaults. |
| 138 | +It's configured via constructor parameters, too. Here they are: |
62 | 139 |
|
63 | | -### [SendAgainMiddleware](../../../src/Middleware/FailureHandling/Implementation/SendAgainMiddleware.php) |
| 140 | +- `id` - A unique string allows to use this strategy more than once for the same message, just like in example above. |
| 141 | +- `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. |
| 142 | + - `delayInitial` - The initial delay that will be applied to a message for the first time. It must be a positive float. |
| 143 | + - `delayMaximum` - The maximum delay which can be applied to a single message. Must be above the `delayInitial`. |
| 144 | + - `exponent` - Message handling delay will be multiplied by exponent each time it fails. |
| 145 | + - `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue channel it came from. When the `queue` parameter is set to `null`, a message will be sent to the same channel it came from. |
64 | 146 |
|
65 | | -This strategy simply resends the given message to a queue. Let's see the constructor parameters through which it's configured: |
| 147 | + Requirements: |
66 | 148 |
|
67 | | -- `id` - A unique string. Allows to use this strategy more than once for the same message, just like in example above. |
68 | | -- `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. |
69 | | -- `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue channel it came from. When the `queue` parameter is set to `null`, a message will be sent to the same channel it came from. |
| 149 | + - Requires a `DelayMiddlewareInterface` implementation and an adapter that supports delayed delivery. |
70 | 150 |
|
71 | | -### [ExponentialDelayMiddleware](../../../src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php) |
| 151 | + State tracking: |
72 | 152 |
|
73 | | -This strategy does the same thing as the `SendAgainMiddleware` with a single difference: it resends a message with an exponentially increasing delay. The delay **must** be implemented by the used `AdapterInterface` implementation. |
| 153 | + - Uses `FailureEnvelope` metadata (`failure-meta`) to store attempts and the previous delay. |
| 154 | + - The per-middleware keys are: |
74 | 155 |
|
75 | | -It's configured via constructor parameters, too. Here they are: |
| 156 | + - `failure-strategy-exponential-delay-attempts-{id}` |
| 157 | + - `failure-strategy-exponential-delay-delay-{id}` |
76 | 158 |
|
77 | | -- `id` - A unique string allows to use this strategy more than once for the same message, just like in example above. |
78 | | -- `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. |
79 | | -- `delayInitial` - The initial delay that will be applied to a message for the first time. It must be a positive float. |
80 | | -- `delayMaximum` - The maximum delay which can be applied to a single message. Must be above the `delayInitial`. |
81 | | -- `exponent` - Message handling delay will be multiplied by exponent each time it fails. |
82 | | -- `queue` - The strategy will send the message to the given queue when it's not `null`. That means you can use this strategy to push a message not to the same queue channel it came from. When the `queue` parameter is set to `null`, a message will be sent to the same channel it came from. |
| 159 | + ## Built-in failure handling components |
| 160 | + |
| 161 | + This package ships the following built-in failure handling components. |
| 162 | + |
| 163 | + ### FailureEnvelope |
| 164 | + |
| 165 | + Class: `Yiisoft\Queue\Middleware\FailureHandling\FailureEnvelope` |
| 166 | + |
| 167 | + Behavior: |
| 168 | + |
| 169 | + - An envelope that stores failure-related metadata under the `failure-meta` key. |
| 170 | + - Built-in failure middlewares use it to persist retry counters / delay parameters across retries. |
| 171 | + |
| 172 | + ### FailureFinalHandler |
| 173 | + |
| 174 | + Class: `Yiisoft\Queue\Middleware\FailureHandling\FailureFinalHandler` |
| 175 | + |
| 176 | + Behavior: |
| 177 | + |
| 178 | + - Terminal failure handler. |
| 179 | + - Throws the exception from the request when the failure pipeline does not handle the failure. |
| 180 | + |
| 181 | + ### JobFailureException |
| 182 | + |
| 183 | + Class: `Yiisoft\Queue\Exception\JobFailureException` |
| 184 | + |
| 185 | + Behavior: |
| 186 | + |
| 187 | + - Thrown by the worker when failure handling does not resolve the issue. |
| 188 | + - Wraps the original exception and includes the queue message id (if available) in the exception message. |
83 | 189 |
|
84 | 190 | ## How to create a custom Failure Middleware? |
85 | 191 |
|
|
0 commit comments