Network communication is inherently unstable, so in a distributed system, a good fault-tolerant design is required. Indiscriminate retry is very dangerous. When there is a problem with communication, each request is retried once, which is equivalent to a 100% increase in system IO load, which is easy to induce avalanche accidents. Retrying also considers the cause of the error. If it is a problem that cannot be solved by retrying, then retrying is just a waste of resources. In addition, if the retrying interface is not idempotent, it may also cause data inconsistency and other problems.
This component provides a rich retry mechanism to meet the retry requirements of various scenarios.
## Install
```bash
composer require hyperf/retry
```
## Hello World
Add the annotation `#[Retry]` to the method that needs to be retried.
```php
/**
* Retry the method on exception
*/
#[Retry]
public function foo()
{
// make a remote call
}
```
The default Retry strategy can meet most daily retry needs without excessive retries causing avalanches.
## Deep customization
This component achieves pluggability by combining multiple retry strategies. Each strategy focuses on different aspects of the retry process, such as retry judgment, retry interval, and result processing. By adjusting the strategy used in the annotation, you can configure the retry aspect suitable for any scenario.
It is recommended to construct your own annotation aliases according to specific business needs. Below we demonstrate how to make a new annotation with a maximum number of attempts of 3.
> In the default `Retry` annotation, you can control the maximum number of retries with `#[Retry(maxAttempts=3)]`. For the sake of demonstration, pretend it doesn't exist.
First you need to create an `annotation class` and inherit `\Hyperf\Retry\Annotations\AbstractRetry`.
```php
<?php
declare(strict_types=1);
namespace App\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
#[Attribute(Attribute::TARGET_METHOD)]
class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry
{
}
```
Override the `$policies` property according to your needs. To limit the number of retries, use `MaxAttemptsRetryPolicy` . `MaxAttemptsRetryPolicy` also needs a parameter, which is the limit of the maximum number of attempts, `$maxAttempts`. Add these two properties to the above class.
```php
<?php
declare(strict_types=1);
namespace App\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
#[Attribute(Attribute::TARGET_METHOD)]
class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry
Now that the `#[MyRetry]` annotation will cause any method to be executed three times in a loop, we also need to add a new policy `ClassifierRetryPolicy` to control what errors can be retried. Adding `ClassifierRetryPolicy` will only retry after throwing `Throwable` by default.
use Doctrine\Common\Annotations\Annotation\Target;
#[Attribute(Attribute::TARGET_METHOD)]
class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry
{
public $policies = [
MaxAttemptsRetryPolicy::class,
ClassifierRetryPolicy::class,
];
public $maxAttempts = 3;
}
```
You can continue to refine the annotation until it meets your customized needs. For example, configure to retry only user-defined `TimeoutException` , and use retry to sleep at least 100ms of variable length interval, as follows:
```php
<?php
declare(strict_types=1);
namespace App\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
#[Attribute(Attribute::TARGET_METHOD)]
class MyRetry extends \Hyperf\Retry\Annotation\Retry
{
public $policies = [
MaxAttemptsRetryPolicy::class,
ClassifierRetryPolicy::class,
SleepRetryPolicy::class,
];
public $maxAttempts = 3;
public $base = 100;
public $strategy = \Hyperf\Retry\BackoffStrategy::class;
public $retryThrowables = [\App\Exception\TimeoutException::class];
Pass the classifier to determine if the error can be retried.
| Parameters | Type | Description |
| ---------- | --- | --- |
| ignoreThrowables | array | `Throwable` class names to ignore. takes precedence over `retryThrowables` |
| retryThrowables | array | `Throwable` class names to retry. takes precedence over `retryOnThrowablePredicate` |
| retryOnThrowablePredicate | callable | Pass a function to determine if `Throwable` can be retried. Returns true if retry is possible, false otherwise. |
| retryOnResultPredicate | callable | Use a function to determine whether the return value can be retried. Returns true if it is possible to retry, false otherwise. |
### Fallback policy `FallbackRetryPolicy`
Execute alternate method after retrying resource exhaustion.
| Parameters | Type | Description |
| ---------- | --- | --- |
| fallback | callable | fallback method |
In addition to the code recognized by `is_callable`, `fallback` can also fill in the format of `class@method`, the framework will get the corresponding `class` from `Container`, and then execute its `method` method .
### Sleep policy `SleepRetryPolicy`
Provides two retry intermittent strategies. Equal retry interval (FlatStrategy) and variable retry interval (BackoffStrategy).
| Parameters | Type | Description |
| ---------- | --- | --- |
| base | int | Base sleep time (ms) |
| strategy | string | Any class name that implements `Hyperf\Retry\SleepStrategyInterface`, such as `Hyperf\Retry\BackoffStrategy` |
### Timeout policy `TimeoutRetryPolicy`
Exit the retry session after the total execution time exceeds the time.
Each `#[Retry]` annotation will generate a corresponding token bucket, and whenever the annotation method is called, a token with an expiration time (ttl) is placed in the token bucket. If a retryable error occurs, the corresponding number of tokens (percentCanRetry) must be consumed before retrying, otherwise it will not be retried (the error continues to pass down). For example, when percentCanRetry=0.2, each retry consumes 5 tokens. In this way, when the peer is down, at most 20% of the additional retry consumption will be incurred, which should be acceptable for most systems.
To take care of some less frequently used methods, a certain number of "mini-guarantee" tokens (minRetriesPerSec) are also generated per second to ensure system stability.
| Parameters | Type | Description |
| ---------- | --- | --- |
| retryBudget.ttl | int | Recovery token expiration time (seconds) |
| retryBudget.minRetriesPerSec | int | Minimum number of retries per second for "mini-guarantee" |
| retryBudget.percentCanRetry | float | Retry times do not exceed the percentage of total requests |
> The token bucket of the retry component is not shared among workers, so the final number of retries is multiplied by the number of workers.
## Annotation alias
Because the retry annotation configuration is more complicated, some preset aliases are provided here for easy writing.