1<?php
2
3namespace GuzzleHttp;
4
5use GuzzleHttp\Promise as P;
6use GuzzleHttp\Promise\PromiseInterface;
7use Psr\Http\Message\RequestInterface;
8use 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 */
16class 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}
120