diff --git a/CHANGELOG.md b/CHANGELOG.md index b63bfca..2b57bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Since v0.10.0 this project adheres to [Semantic Versioning](http://semver.org/) #### Added +* Support for the maxlag parameter (with retries) in API requests ([#112]) * Support for getting/setting user agent for API requests ([#107]) * Added missing PHPDoc comments for properties, constants, and more ([#109]) @@ -129,4 +130,5 @@ Since v0.10.0 this project adheres to [Semantic Versioning](http://semver.org/) [#108]: https://github.com/hamstar/Wikimate/pull/108 [#109]: https://github.com/hamstar/Wikimate/pull/109 [#111]: https://github.com/hamstar/Wikimate/pull/111 +[#112]: https://github.com/hamstar/Wikimate/pull/112 [#114]: https://github.com/hamstar/Wikimate/pull/114 diff --git a/Wikimate.php b/Wikimate.php index 158b43a..6cbc877 100644 --- a/Wikimate.php +++ b/Wikimate.php @@ -10,6 +10,10 @@ /** * Provides an interface over wiki API objects such as pages and files. * + * All requests to the API can throw an exception if the server is lagged + * and a finite number of retries is exhausted. By default requests are + * retried indefinitely. See {@see Wikimate::request()} for more information. + * * @author Robert McLeod & Frans P. de Vries * @since 0.2 December 2010 */ @@ -39,6 +43,14 @@ class Wikimate */ const TOKEN_LOGIN = 'login'; + /** + * Default lag value in seconds + * + * @var integer + * @link https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Maxlag_parameter + */ + const MAXLAG_DEFAULT = 5; + /** * Base URL for API requests * @@ -93,6 +105,21 @@ class Wikimate */ protected $debugMode = false; + /** + * Maximum lag in seconds to accept in requests + * + * @var integer + * @link https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Maxlag_parameter + */ + protected $maxlag = self::MAXLAG_DEFAULT; + + /** + * Maximum number of retries for lagged requests (-1 = retry indefinitely) + * + * @var integer + */ + protected $maxretries = -1; + /** * Creates a new Wikimate object. * @@ -125,6 +152,78 @@ protected function initRequests() $this->session->useragent = $this->useragent; } + /** + * Sends a GET or POST request in JSON format to the API. + * + * This method handles maxlag errors as advised at: + * {@see https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Maxlag_parameter) + * The request is sent with the current maxlag value + * (default: 5 seconds, per MAXLAG_DEFAULT). + * If a lag error is received, the method waits (sleeps) for the + * recommended time (per the Retry-After header), then tries again. + * It will do this indefinitely unless the number of retries is limited, + * in which case an exception is thrown once the limit is reached. + * + * The string type for $data is used only for upload POST requests, + * and must contain the complete multipart body, including maxlag. + * + * @param array|string $data Data for the request + * @param array $headers Optional extra headers to send with the request + * @param boolean $post True to send a POST request, otherwise GET + * @return Requests_Response The API response + * @throw Exception If lagged and ran out of retries + */ + private function request($data, $headers = array(), $post = false) + { + $retries = 0; + + // Add format & maxlag parameter to request + if (is_array($data)) { + $data['format'] = 'json'; + $data['maxlag'] = $this->getMaxlag(); + } + + // Send appropriate type of request, once or multiple times + do { + if ($post) { + $response = $this->session->post($this->api, $headers, $data); + } else { + $response = $this->session->get($this->api.'?'.http_build_query($data), $headers); + } + + // Check for replication lag error + $server_lagged = ($response->headers->offsetGet('X-Database-Lag') !== null); + if ($server_lagged) { + // Determine recommended or default delay + if ($response->headers->offsetGet('Retry-After') !== null) { + $sleep = (int)$response->headers->offsetGet('Retry-After'); + } else { + $sleep = $this->getMaxlag(); + } + + if ($this->debugMode) { + preg_match('/Waiting for [^ ]*: ([0-9.-]+) seconds? lagged/', $response->body, $match); + echo "Server lagged for {$match[1]} seconds; will retry in {$sleep} seconds\n"; + } + sleep($sleep); + + // Check retries limit + if ($this->getMaxretries() >= 0) { + $retries++; + } else { + $retries = -1; // continue indefinitely + } + } + } while ($server_lagged && $retries <= $this->getMaxretries()); + + // Throw exception if we ran out of retries + if ($server_lagged) { + throw new Exception("Server lagged ($retries consecutive maxlag responses)"); + } else { + return $response; + } + } + /** * Obtains a wiki token for logging in or data-modifying actions. * For now this method, in Wikimate tradition, is kept simple and supports @@ -149,11 +248,11 @@ protected function token($type = self::TOKEN_DEFAULT) 'action' => 'query', 'meta' => 'tokens', 'type' => $type, - 'format' => 'json' ); // Send the token request - $response = $this->session->post($this->api, array(), $details); + $response = $this->request($details, array(), true); + // Check if we got an API result or the API doc page (invalid request) if (strpos($response->body, "This is an auto-generated MediaWiki API documentation page") !== false) { $this->error = array(); @@ -204,7 +303,6 @@ public function login($username, $password, $domain = null) 'lgname' => $username, 'lgpassword' => $password, 'lgtoken' => $logintoken, - 'format' => 'json' ); // If $domain is provided, set the corresponding detail in the request information array @@ -213,7 +311,8 @@ public function login($username, $password, $domain = null) } // Send the login request - $response = $this->session->post($this->api, array(), $details); + $response = $this->request($details, array(), true); + // Check if we got an API result or the API doc page (invalid request) if (strpos($response->body, "This is an auto-generated MediaWiki API documentation page") !== false) { $this->error = array(); @@ -253,6 +352,50 @@ public function login($username, $password, $domain = null) return true; } + /** + * Gets the current value of the maxlag parameter. + * + * @return integer The maxlag value in seconds + */ + public function getMaxlag() + { + return $this->maxlag; + } + + /** + * Sets the new value of the maxlag parameter. + * + * @param integer $ml The new maxlag value in seconds + * @return Wikimate This object + */ + public function setMaxlag($ml) + { + $this->maxlag = (int)$ml; + return $this; + } + + /** + * Gets the current value of the max retries limit. + * + * @return integer The max retries limit + */ + public function getMaxretries() + { + return $this->maxretries; + } + + /** + * Sets the new value of the max retries limit. + * + * @param integer $mr The new max retries limit + * @return Wikimate This object + */ + public function setMaxretries($mr) + { + $this->maxretries = (int)$mr; + return $this; + } + /** * Gets the user agent for API requests. * @@ -361,13 +504,12 @@ public function getFile($filename) public function query($array) { $array['action'] = 'query'; - $array['format'] = 'json'; if ($this->debugMode) { echo "query GET parameters:\n"; echo http_build_query($array) . "\n"; } - $apiResult = $this->session->get($this->api.'?'.http_build_query($array)); + $apiResult = $this->request($array); if ($this->debugMode) { echo "query GET response:\n"; @@ -386,13 +528,12 @@ public function query($array) public function parse($array) { $array['action'] = 'parse'; - $array['format'] = 'json'; if ($this->debugMode) { echo "parse GET parameters:\n"; echo http_build_query($array) . "\n"; } - $apiResult = $this->session->get($this->api.'?'.http_build_query($array)); + $apiResult = $this->request($array); if ($this->debugMode) { echo "parse GET response:\n"; @@ -420,14 +561,13 @@ public function edit($array) ); $array['action'] = 'edit'; - $array['format'] = 'json'; $array['token'] = $edittoken; if ($this->debugMode) { echo "edit POST parameters:\n"; print_r($array); } - $apiResult = $this->session->post($this->api, $headers, $array); + $apiResult = $this->request($array, $headers, true); if ($this->debugMode) { echo "edit POST response:\n"; @@ -455,14 +595,13 @@ public function delete($array) ); $array['action'] = 'delete'; - $array['format'] = 'json'; $array['token'] = $deletetoken; if ($this->debugMode) { echo "delete POST parameters:\n"; print_r($array); } - $apiResult = $this->session->post($this->api, $headers, $array); + $apiResult = $this->request($array, $headers, true); if ($this->debugMode) { echo "delete POST response:\n"; @@ -510,6 +649,7 @@ public function upload($array) $array['action'] = 'upload'; $array['format'] = 'json'; + $array['maxlag'] = $this->getMaxlag(); $array['token'] = $uploadtoken; // Construct multipart body: @@ -541,7 +681,7 @@ public function upload($array) 'Content-Length' => strlen($body), ); - $apiResult = $this->session->post($this->api, $headers, $body); + $apiResult = $this->request($body, $headers, true); if ($this->debugMode) { echo "upload POST response:\n";