Completed
Push — ciEnhancements ( 259e21...456a90 )
by adam
10:44 queued 08:54
created

MiddlewareFactory::getRetryDelay()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.0534
cc 4
eloc 10
nc 1
nop 0
crap 4
1
<?php
2
3
namespace Mediawiki\Api\Guzzle;
4
5
use GuzzleHttp\Exception\ConnectException;
6
use GuzzleHttp\Exception\RequestException;
7
use GuzzleHttp\Middleware;
8
use GuzzleHttp\Psr7\Request;
9
use GuzzleHttp\Psr7\Response;
10
use Psr\Log\LoggerAwareInterface;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\NullLogger;
13
14
/**
15
 * @access private
16
 *
17
 * @author Addshore
18
 */
19
class MiddlewareFactory implements LoggerAwareInterface {
20
21
	/**
22
	 * @var LoggerInterface
23
	 */
24
	private $logger;
25
26 10
	public function __construct() {
27 10
		$this->logger = new NullLogger();
28 10
	}
29
30
	public function setLogger( LoggerInterface $logger ) {
31
		$this->logger = $logger;
32
	}
33
34
	/**
35
	 * @access private
36
	 *
37
	 * @param bool $delay default to true, can be false to speed up tests
38
	 *
39
	 * @return callable
40
	 */
41 10
	public function retry( $delay = true ) {
42 10
		if( $delay ) {
43 10
			return Middleware::retry( $this->newRetryDecider(), $this->getRetryDelay() );
44
		} else {
45
			return Middleware::retry( $this->newRetryDecider() );
46
		}
47
	}
48
49
	/**
50
	 * Returns a method that takes the number of retries and returns the number of miliseconds
51
	 * to wait
52
	 *
53
	 * @return callable
54
	 */
55 10
	private function getRetryDelay() {
56
		return function( $numberOfRetries, Response $response = null ) {
57
			// The $response argument is only passed as of Guzzle 6.2.2.
58 10
			if( $response !== null ) {
59
				// Retry-After may be a number of seconds or an absolute date (RFC 7231,
60
				// section 7.1.3).
61 7
				$retryAfter = $response->getHeaderLine( 'Retry-After' );
62
63 7
				if( is_numeric( $retryAfter ) ) {
64 1
					return 1000 * $retryAfter;
65
				}
66
67 6
				if( $retryAfter ) {
68 2
					$seconds = strtotime( $retryAfter ) - time();
69 2
					return 1000 * max( 1, $seconds );
70
				}
71 4
			}
72
73 7
			return 1000 * $numberOfRetries;
74 10
		};
75
	}
76
77
	/**
78
	 * @return callable
79
	 */
80
	private function newRetryDecider() {
81 10
		return function (
82
			$retries,
83
			Request $request,
84
			Response $response = null,
85 1
			RequestException $exception = null
86
		) {
87
			// Don't retry if we have run out of retries
88 10
			if ( $retries >= 5 ) {
89 1
				return false;
90
			}
91
92 10
			$shouldRetry = false;
93
94
			// Retry connection exceptions
95 10
			if( $exception instanceof ConnectException ) {
96 3
				$shouldRetry = true;
97 3
			}
98
99 10
			if( $response ) {
100 9
				$data = json_decode( $response->getBody(), true );
101
102
				// Retry on server errors
103 9
				if( $response->getStatusCode() >= 500 ) {
104 2
					$shouldRetry = true;
105 2
				}
106
107 9
				foreach( $response->getHeader( 'Mediawiki-Api-Error' ) as $mediawikiApiErrorHeader ) {
108
					if (
109
						// Retry if the API explicitly tells us to:
110
						// https://www.mediawiki.org/wiki/Manual:Maxlag_parameter
111 5
						$response->getHeaderLine( 'Retry-After' )
112
						||
113
						// Retry if we have a response with an API error worth retrying
114 2
						in_array(
115 2
							$mediawikiApiErrorHeader,
116
							array(
117 2
								'ratelimited',
118 2
								'maxlag',
119 2
								'readonly',
120 2
								'internal_api_error_DBQueryError',
121
							)
122 2
						)
123 2
						||
124
						// Or if we have been stopped from saving as an 'anti-abuse measure'
125
						// Note: this tries to match "actionthrottledtext" i18n messagae for mediawiki
126
						(
127 2
							$mediawikiApiErrorHeader == 'failed-save' &&
128 1
							strstr( $data['error']['info'], 'anti-abuse measure' )
129 1
						)
130 5
					) {
131 5
						$shouldRetry = true;
132 5
					}
133
134 9
				}
135 9
			}
136
137
			// Log if we are retrying
138 10
			if( $shouldRetry ) {
139 10
				$this->logger->warning(
140 10
					sprintf(
141 10
						'Retrying %s %s %s/5, %s',
142 10
						$request->getMethod(),
143 10
						$request->getUri(),
144 10
						$retries + 1,
145 10
						$response ? 'status code: ' . $response->getStatusCode() :
146 3
							$exception->getMessage()
0 ignored issues
show
Bug introduced by
It seems like $exception is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
147 10
					)
148 10
				);
149 10
			}
150
151 10
			return $shouldRetry;
152 10
		};
153
	}
154
155
}
156