1 | <?php
|
---|
2 |
|
---|
3 | namespace GuzzleHttp;
|
---|
4 |
|
---|
5 | use GuzzleHttp\Promise as P;
|
---|
6 | use GuzzleHttp\Promise\PromiseInterface;
|
---|
7 | use Psr\Http\Message\RequestInterface;
|
---|
8 | use Psr\Http\Message\ResponseInterface;
|
---|
9 |
|
---|
10 | /**
|
---|
11 | * Middleware that retries requests based on the boolean result of
|
---|
12 | * invoking the provided "decider" function.
|
---|
13 | *
|
---|
14 | * @final
|
---|
15 | */
|
---|
16 | class RetryMiddleware
|
---|
17 | {
|
---|
18 | /**
|
---|
19 | * @var callable(RequestInterface, array): PromiseInterface
|
---|
20 | */
|
---|
21 | private $nextHandler;
|
---|
22 |
|
---|
23 | /**
|
---|
24 | * @var callable
|
---|
25 | */
|
---|
26 | private $decider;
|
---|
27 |
|
---|
28 | /**
|
---|
29 | * @var callable(int)
|
---|
30 | */
|
---|
31 | private $delay;
|
---|
32 |
|
---|
33 | /**
|
---|
34 | * @param callable $decider Function that accepts the number of retries,
|
---|
35 | * a request, [response], and [exception] and
|
---|
36 | * returns true if the request is to be
|
---|
37 | * retried.
|
---|
38 | * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke.
|
---|
39 | * @param (callable(int): int)|null $delay Function that accepts the number of retries
|
---|
40 | * and returns the number of
|
---|
41 | * milliseconds to delay.
|
---|
42 | */
|
---|
43 | public function __construct(callable $decider, callable $nextHandler, ?callable $delay = null)
|
---|
44 | {
|
---|
45 | $this->decider = $decider;
|
---|
46 | $this->nextHandler = $nextHandler;
|
---|
47 | $this->delay = $delay ?: __CLASS__.'::exponentialDelay';
|
---|
48 | }
|
---|
49 |
|
---|
50 | /**
|
---|
51 | * Default exponential backoff delay function.
|
---|
52 | *
|
---|
53 | * @return int milliseconds.
|
---|
54 | */
|
---|
55 | public static function exponentialDelay(int $retries): int
|
---|
56 | {
|
---|
57 | return (int) 2 ** ($retries - 1) * 1000;
|
---|
58 | }
|
---|
59 |
|
---|
60 | public function __invoke(RequestInterface $request, array $options): PromiseInterface
|
---|
61 | {
|
---|
62 | if (!isset($options['retries'])) {
|
---|
63 | $options['retries'] = 0;
|
---|
64 | }
|
---|
65 |
|
---|
66 | $fn = $this->nextHandler;
|
---|
67 |
|
---|
68 | return $fn($request, $options)
|
---|
69 | ->then(
|
---|
70 | $this->onFulfilled($request, $options),
|
---|
71 | $this->onRejected($request, $options)
|
---|
72 | );
|
---|
73 | }
|
---|
74 |
|
---|
75 | /**
|
---|
76 | * Execute fulfilled closure
|
---|
77 | */
|
---|
78 | private function onFulfilled(RequestInterface $request, array $options): callable
|
---|
79 | {
|
---|
80 | return function ($value) use ($request, $options) {
|
---|
81 | if (!($this->decider)(
|
---|
82 | $options['retries'],
|
---|
83 | $request,
|
---|
84 | $value,
|
---|
85 | null
|
---|
86 | )) {
|
---|
87 | return $value;
|
---|
88 | }
|
---|
89 |
|
---|
90 | return $this->doRetry($request, $options, $value);
|
---|
91 | };
|
---|
92 | }
|
---|
93 |
|
---|
94 | /**
|
---|
95 | * Execute rejected closure
|
---|
96 | */
|
---|
97 | private function onRejected(RequestInterface $req, array $options): callable
|
---|
98 | {
|
---|
99 | return function ($reason) use ($req, $options) {
|
---|
100 | if (!($this->decider)(
|
---|
101 | $options['retries'],
|
---|
102 | $req,
|
---|
103 | null,
|
---|
104 | $reason
|
---|
105 | )) {
|
---|
106 | return P\Create::rejectionFor($reason);
|
---|
107 | }
|
---|
108 |
|
---|
109 | return $this->doRetry($req, $options);
|
---|
110 | };
|
---|
111 | }
|
---|
112 |
|
---|
113 | private function doRetry(RequestInterface $request, array $options, ?ResponseInterface $response = null): PromiseInterface
|
---|
114 | {
|
---|
115 | $options['delay'] = ($this->delay)(++$options['retries'], $response, $request);
|
---|
116 |
|
---|
117 | return $this($request, $options);
|
---|
118 | }
|
---|
119 | }
|
---|