Completed
Push — master ( bd9812...9da616 )
by Márk
02:24
created

RetryPlugin::retry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.9332
c 0
b 0
f 0
cc 1
nc 1
nop 5
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Http\Client\Common\Plugin;
6
7
use Http\Client\Common\Plugin;
8
use Http\Client\Exception;
9
use Http\Client\Exception\HttpException;
10
use Http\Promise\Promise;
11
use Psr\Http\Message\RequestInterface;
12
use Psr\Http\Message\ResponseInterface;
13
use Symfony\Component\OptionsResolver\OptionsResolver;
14
15
/**
16
 * Retry the request if an exception is thrown.
17
 *
18
 * By default will retry only one time.
19
 *
20
 * @author Joel Wurtz <[email protected]>
21
 */
22
final class RetryPlugin implements Plugin
23
{
24
    /**
25
     * Number of retry before sending an exception.
26
     *
27
     * @var int
28
     */
29
    private $retry;
30
31
    /**
32
     * @var callable
33
     */
34
    private $errorResponseDelay;
35
36
    /**
37
     * @var callable
38
     */
39
    private $errorResponseDecider;
40
41
    /**
42
     * @var callable
43
     */
44
    private $exceptionDecider;
45
46
    /**
47
     * @var callable
48
     */
49
    private $exceptionDelay;
50
51
    /**
52
     * Store the retry counter for each request.
53
     *
54
     * @var array
55
     */
56
    private $retryStorage = [];
57
58
    /**
59
     * @param array $config {
60
     *
61
     *     @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up
62
     *     @var callable $error_response_decider A callback that gets a request and response to decide whether the request should be retried
63
     *     @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried
64
     *     @var callable $error_response_delay A callback that gets a request and response and the current number of retries and returns how many microseconds we should wait before trying again
65
     *     @var callable $exception_delay A callback that gets a request, an exception and the current number of retries and returns how many microseconds we should wait before trying again
66
     * }
67
     */
68 10
    public function __construct(array $config = [])
69
    {
70 10
        $resolver = new OptionsResolver();
71 10
        $resolver->setDefaults([
72 10
            'retries' => 1,
73
            'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) {
74
                // do not retry client errors
75 1
                return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600;
76 10
            },
77
            'exception_decider' => function (RequestInterface $request, Exception $e) {
78
                // do not retry client errors
79 4
                return !$e instanceof HttpException || $e->getCode() >= 500 && $e->getCode() < 600;
80 10
            },
81
            'error_response_delay' => __CLASS__.'::defaultErrorResponseDelay',
82
            'exception_delay' => __CLASS__.'::defaultExceptionDelay',
83
        ]);
84
85 10
        $resolver->setAllowedTypes('retries', 'int');
86 10
        $resolver->setAllowedTypes('error_response_decider', 'callable');
87 10
        $resolver->setAllowedTypes('exception_decider', 'callable');
88 10
        $resolver->setAllowedTypes('error_response_delay', 'callable');
89 10
        $resolver->setAllowedTypes('exception_delay', 'callable');
90 10
        $options = $resolver->resolve($config);
91
92 10
        $this->retry = $options['retries'];
93 10
        $this->errorResponseDecider = $options['error_response_decider'];
94 10
        $this->errorResponseDelay = $options['error_response_delay'];
95 10
        $this->exceptionDecider = $options['exception_decider'];
96 10
        $this->exceptionDelay = $options['exception_delay'];
97 10
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 6
    public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
103
    {
104 6
        $chainIdentifier = spl_object_hash((object) $first);
105
106
        return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) {
107 3
            if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
108 1
                $this->retryStorage[$chainIdentifier] = 0;
109
            }
110
111 3
            if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
112 2
                unset($this->retryStorage[$chainIdentifier]);
113
114 2
                return $response;
115
            }
116
117 1
            if (call_user_func($this->errorResponseDecider, $request, $response)) {
118
                $time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]);
119
                $response = $this->retry($request, $next, $first, $chainIdentifier, $time);
120
            }
121
122 1
            if (array_key_exists($chainIdentifier, $this->retryStorage)) {
123 1
                unset($this->retryStorage[$chainIdentifier]);
124
            }
125
126 1
            return $response;
127
        }, function (Exception $exception) use ($request, $next, $first, $chainIdentifier) {
128 5
            if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
129 5
                $this->retryStorage[$chainIdentifier] = 0;
130
            }
131
132 5
            if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
133 1
                unset($this->retryStorage[$chainIdentifier]);
134
135 1
                throw $exception;
136
            }
137
138 5
            if (!call_user_func($this->exceptionDecider, $request, $exception)) {
139 2
                throw $exception;
140
            }
141
142 3
            $time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]);
143
144 3
            return $this->retry($request, $next, $first, $chainIdentifier, $time);
145 6
        });
146
    }
147
148
    /**
149
     * @param int $retries The number of retries we made before. First time this get called it will be 0.
150
     */
151 1
    public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, int $retries): int
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $response is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
152
    {
153 1
        return pow(2, $retries) * 500000;
154
    }
155
156
    /**
157
     * @param int $retries The number of retries we made before. First time this get called it will be 0.
158
     */
159 4
    public static function defaultExceptionDelay(RequestInterface $request, Exception $e, int $retries): int
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $e is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
160
    {
161 4
        return pow(2, $retries) * 500000;
162
    }
163
164
    /**
165
     * @throws \Exception if retrying returns a failed promise
166
     */
167 3
    private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay): ResponseInterface
168
    {
169 3
        usleep($delay);
170
171
        // Retry synchronously
172 3
        ++$this->retryStorage[$chainIdentifier];
173 3
        $promise = $this->handleRequest($request, $next, $first);
174
175 3
        return $promise->wait();
176
    }
177
}
178