|
| 1 | +# Error handling on message processing |
| 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/yii-queue` with _Failure Handling Middleware Pipeline_. It is triggered each time message processing via Consume Middleware Pipeline is interrupted with any `Throwable`. |
| 4 | + |
| 5 | +## Configuration |
| 6 | + |
| 7 | +Here below is configuration via `yiisoft/config`. If you don't use it - you should add middleware definition list (in the `middlewares-fail` key here) to the `FailureMiddlewareDispatcher` by your own. |
| 8 | + |
| 9 | +Configuration should be passed to the `yiisoft/yii-queue.fail-strategy-pipelines` key of the `params` config to work with the `yiisoft/config`. You can define different failure handling pipelines for each queue channel. Let's see and describe an example: |
| 10 | + |
| 11 | +```php |
| 12 | +'yiisoft/yii-queue' => [ |
| 13 | + 'middlewares-fail' => [ |
| 14 | + FailureMiddlewareDispatcher::DEFAULT_PIPELINE => [ |
| 15 | + [ |
| 16 | + 'class' => SendAgainMiddleware::class, |
| 17 | + '__construct()' => ['id' => 'default-first-resend', 'queue' => null], |
| 18 | + ], |
| 19 | + static fn (QueueFactoryInterface $factory) => new SendAgainMiddleware( |
| 20 | + id: 'default-second-resend', |
| 21 | + queue: $factory->get('failed-messages'), |
| 22 | + ), |
| 23 | + ], |
| 24 | + |
| 25 | + 'failed-messages' => [ |
| 26 | + [ |
| 27 | + 'class' => ExponentialDelayMiddleware::class, |
| 28 | + '__construct()' => [ |
| 29 | + 'id' => 'failed-messages', |
| 30 | + 'maxAttempts' => 30, |
| 31 | + 'delayInitial' => 5, |
| 32 | + 'delayMaximum' => 60, |
| 33 | + 'exponent' => 1.5, |
| 34 | + 'queue' => null, |
| 35 | + ], |
| 36 | + ], |
| 37 | + ], |
| 38 | + ], |
| 39 | +] |
| 40 | +``` |
| 41 | + |
| 42 | +Keys here except `FailureMiddlewareDispatcher::DEFAULT_PIPELINE` are queue channel names, and values are lists of `FailureMiddlewareInterface` definitions. `FailureMiddlewareDispatcher::DEFAULT_PIPELINE` defines a default pipeline to apply to channels without explicitly defined failure strategy pipeline. Each middleware definition must be one of: |
| 43 | +- A ready-to-use `MiddlewareFailureInterface` object like `new FooMiddleware()`. |
| 44 | +- A valid definition for the [yiisoft/definitions](https://github.com/yiisoft/definitions). It must describe an object, implementing the `MiddlewareFailureInterface`. |
| 45 | +- A callable: `fn() => // do stuff`, `$object->foo(...)`, etc. It will be executed through the `yiisoft/injector`, so all the dependencies of your callable will be resolved. You can also define a "callable-looking" array, where object will be instantiated with a DI container: `[FooMiddleware::class, 'handle']`. |
| 46 | +- A string for your DI container to resolve the middleware, e.g. `FooMiddleware::class`. |
| 47 | + |
| 48 | +In the example above failures will be handled this way (look the concrete middleware description below): |
| 49 | + |
| 50 | +1. For the first time message will be resent to the same queue channel immediately. |
| 51 | +2. If it fails again, it will be resent to the queue channel named `failed-messages`. |
| 52 | +3. From now on it will be resent to the same queue channel (`failed-messages`) up to 30 times with a delay from 5 to 60 seconds, increased 1.5 times each time the message fails again. |
| 53 | +4. If the message handler throw an exception one more (33rd) time, the exception will not be caught. |
| 54 | + |
| 55 | +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. |
| 56 | + |
| 57 | +## Default failure handling strategies |
| 58 | + |
| 59 | +Let's see the built-in defaults. |
| 60 | + |
| 61 | +### SendAgainMiddleware |
| 62 | + |
| 63 | +This strategy simply resends the given message to a queue. Let's see the constructor parameters through which it's configured: |
| 64 | + |
| 65 | +- `id` - A unique string. Allows to use this strategy more than once for the same message, just like in example above. |
| 66 | +- `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. |
| 67 | +- `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. |
| 68 | + |
| 69 | +### ExponentialDelayMiddleware |
| 70 | + |
| 71 | +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. |
| 72 | + |
| 73 | +It's configured via constructor parameters, too. Here they are: |
| 74 | + |
| 75 | +- `id` - A unique string allows to use this strategy more than once for the same message, just like in example above. |
| 76 | +- `maxAttempts` - Maximum attempts count for this strategy with the given $id before it will give up. |
| 77 | +- `delayInitial` - The initial delay that will be applied to a message for the first time. It must be a positive float. |
| 78 | +- `delayMaximum` - The maximum delay which can be applied to a single message. Must be above the `delayInitial`. |
| 79 | +- `exponent` - Message handling delay will be muliplied by exponent each time it fails. |
| 80 | +- `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. |
| 81 | + |
| 82 | +## How to create a custom Failure Middleware? |
| 83 | + |
| 84 | +All you need is to implement the `MiddlewareFailureInterface` and add your implementation definition to the [configuration](#configuration). |
| 85 | +This interface has the only method `handle`. And the method has these parameters: |
| 86 | +- `ConsumeRequest $request` - a request for a message handling. It consists of a message and a queue the message came from. |
| 87 | +- `Throwable $exception` - an exception thrown on the `request` handling |
| 88 | +- `MessageFailureHandlerInterface $handler` - failure strategy pipeline continuation. Your Middleware should call `$pipeline->handle()` when it shouldn't interrupt failure pipeline execution. |
| 89 | + |
| 90 | +> Note: your strategy have to check by its own if it should be applied. Look into [`SendAgainMiddleware::suites()`](../../src/Middleware/Implementation/FailureMiddleware/Middleware/SendAgainMiddleware.php#L52) for an example. |
0 commit comments