Completed
Push — master ( 00f4c3...955d05 )
by David
03:34
created

RetryPlugin::__construct()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6.4222

Importance

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