Completed
Push — main ( 26c7b4...5ab5c5 )
by
unknown
04:18
created

MiddlewareFactory::getRetryDelay()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
c 0
b 0
f 0
rs 9.584
cc 4
nc 1
nop 0
1
<?php
2
3
namespace Addwiki\Mediawiki\Api\Guzzle;
4
5
use GuzzleHttp\Exception\ConnectException;
6
use GuzzleHttp\Exception\TransferException;
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
	private NullLogger $logger;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
22
23
	public function __construct() {
24
		$this->logger = new NullLogger();
25
	}
26
27
	/**
28
	 * @param LoggerInterface $logger The new Logger object.
29
	 */
30
	public function setLogger( LoggerInterface $logger ) {
31
		$this->logger = $logger;
32
	}
33
34
	/**
35
	 * @private
36
	 *
37
	 * @param bool $delay default to true, can be false to speed up tests
38
	 */
39
	public function retry( bool $delay = true ) : callable {
40
		if ( $delay ) {
41
			return Middleware::retry( $this->newRetryDecider(), $this->getRetryDelay() );
42
		} else {
43
			return Middleware::retry( $this->newRetryDecider() );
44
		}
45
	}
46
47
	/**
48
	 * Returns a method that takes the number of retries and returns the number of miliseconds
49
	 * to wait
50
	 */
51
	private function getRetryDelay(): callable {
52
		return function ( $numberOfRetries, Response $response = null ) {
53
			// The $response argument is only passed as of Guzzle 6.2.2.
54
			if ( $response !== null ) {
55
				// Retry-After may be a number of seconds or an absolute date (RFC 7231,
56
				// section 7.1.3).
57
				$retryAfter = $response->getHeaderLine( 'Retry-After' );
58
59
				if ( is_numeric( $retryAfter ) ) {
60
					return 1000 * $retryAfter;
61
				}
62
63
				if ( $retryAfter !== '' ) {
64
					$seconds = strtotime( $retryAfter ) - time();
65
					return 1000 * max( 1, $seconds );
66
				}
67
			}
68
69
			return 1000 * $numberOfRetries;
70
		};
71
	}
72
73
	private function newRetryDecider(): callable {
74
		return function (
75
			$retries,
76
			Request $request,
77
			Response $response = null,
78
			TransferException $exception = null
79
		): bool {
80
			// Don't retry if we have run out of retries
81
			if ( $retries >= 5 ) {
82
				return false;
83
			}
84
85
			$shouldRetry = false;
86
87
			// Retry connection exceptions
88
			if ( $exception instanceof ConnectException ) {
89
				$shouldRetry = true;
90
			}
91
92
			if ( $response !== null ) {
93
				$data = json_decode( $response->getBody(), true );
94
95
				// Retry on server errors
96
				if ( $response->getStatusCode() >= 500 ) {
97
					$shouldRetry = true;
98
				}
99
100
				foreach ( $response->getHeader( 'Mediawiki-Api-Error' ) as $mediawikiApiErrorHeader ) {
101
					$RetryAfterResponseHeaderLine = $response->getHeaderLine( 'Retry-After' );
102
					if (
103
						// Retry if the API explicitly tells us to:
104
						// https://www.mediawiki.org/wiki/Manual:Maxlag_parameter
105
						$RetryAfterResponseHeaderLine
106
						||
107
						// Retry if we have a response with an API error worth retrying
108
						in_array(
109
							$mediawikiApiErrorHeader,
110
							[
111
								'ratelimited',
112
								'maxlag',
113
								'readonly',
114
								'internal_api_error_DBQueryError',
115
							]
116
						)
117
						||
118
						// Or if we have been stopped from saving as an 'anti-abuse measure'
119
						// Note: this tries to match "actionthrottledtext" i18n messagae for mediawiki
120
						(
121
							$mediawikiApiErrorHeader == 'failed-save' &&
122
							strstr( $data['error']['info'], 'anti-abuse measure' )
123
						)
124
					) {
125
						$shouldRetry = true;
126
					}
127
128
				}
129
			}
130
131
			// Log if we are retrying
132
			if ( $shouldRetry ) {
133
				$this->logger->warning(
134
					sprintf(
135
						'Retrying %s %s %s/5, %s',
136
						$request->getMethod(),
137
						$request->getUri(),
138
						$retries + 1,
139
						$response !== null ? 'status code: ' . $response->getStatusCode() :
140
							$exception->getMessage()
141
					)
142
				);
143
			}
144
145
			return $shouldRetry;
146
		};
147
	}
148
149
}
150