Skip to content

Commit c9afc3e

Browse files
committed
Merge pull request googleapis#416 from whatthejeff/exponential-backoff
Add support for retrying failed requests with exponential backoff.
2 parents f75e837 + aaaa179 commit c9afc3e

File tree

9 files changed

+1230
-7
lines changed

9 files changed

+1230
-7
lines changed

src/Google/Config.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class Google_Config
2525
const GZIP_UPLOADS_ENABLED = true;
2626
const GZIP_UPLOADS_DISABLED = false;
2727
const USE_AUTO_IO_SELECTION = "auto";
28+
const TASK_RETRY_NEVER = 0;
29+
const TASK_RETRY_ONCE = 1;
30+
const TASK_RETRY_ALWAYS = -1;
2831
protected $configuration;
2932

3033
/**
@@ -101,6 +104,36 @@ public function __construct($ini_file_location = null)
101104
'federated_signon_certs_url' =>
102105
'https://www.googleapis.com/oauth2/v1/certs',
103106
),
107+
'Google_Task_Runner' => array(
108+
// Delays are specified in seconds
109+
'initial_delay' => 1,
110+
'max_delay' => 60,
111+
// Base number for exponential backoff
112+
'factor' => 2,
113+
// A random number between -jitter and jitter will be added to the
114+
// factor on each iteration to allow for better distribution of
115+
// retries.
116+
'jitter' => .5,
117+
// Maximum number of retries allowed
118+
'retries' => 0
119+
),
120+
'Google_Service_Exception' => array(
121+
'retry_map' => array(
122+
'500' => self::TASK_RETRY_ALWAYS,
123+
'503' => self::TASK_RETRY_ALWAYS,
124+
'rateLimitExceeded' => self::TASK_RETRY_ALWAYS,
125+
'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS
126+
)
127+
),
128+
'Google_IO_Exception' => array(
129+
'retry_map' => array(
130+
CURLE_COULDNT_RESOLVE_HOST => self::TASK_RETRY_ALWAYS,
131+
CURLE_COULDNT_CONNECT => self::TASK_RETRY_ALWAYS,
132+
CURLE_OPERATION_TIMEOUTED => self::TASK_RETRY_ALWAYS,
133+
CURLE_SSL_CONNECT_ERROR => self::TASK_RETRY_ALWAYS,
134+
CURLE_GOT_NOTHING => self::TASK_RETRY_ALWAYS
135+
)
136+
),
104137
// Set a default directory for the file cache.
105138
'Google_Cache_File' => array(
106139
'directory' => sys_get_temp_dir() . '/Google_Client'

src/Google/Http/REST.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
class Google_Http_REST
2727
{
2828
/**
29-
* Executes a Google_Http_Request
29+
* Executes a Google_Http_Request and (if applicable) automatically retries
30+
* when errors occur.
3031
*
3132
* @param Google_Client $client
3233
* @param Google_Http_Request $req
@@ -35,6 +36,27 @@ class Google_Http_REST
3536
* invalid or malformed post body, invalid url)
3637
*/
3738
public static function execute(Google_Client $client, Google_Http_Request $req)
39+
{
40+
$runner = new Google_Task_Runner(
41+
$client,
42+
sprintf('%s %s', $req->getRequestMethod(), $req->getUrl()),
43+
array(get_class(), 'doExecute'),
44+
array($client, $req)
45+
);
46+
47+
return $runner->run();
48+
}
49+
50+
/**
51+
* Executes a Google_Http_Request
52+
*
53+
* @param Google_Client $client
54+
* @param Google_Http_Request $req
55+
* @return array decoded result
56+
* @throws Google_Service_Exception on server side error (ie: not authenticated,
57+
* invalid or malformed post body, invalid url)
58+
*/
59+
public static function doExecute(Google_Client $client, Google_Http_Request $req)
3860
{
3961
$httpRequest = $client->getIo()->makeRequest($req);
4062
$httpRequest->setExpectedClass($req->getExpectedClass());
@@ -74,13 +96,19 @@ public static function decodeHttpResponse($response, Google_Client $client = nul
7496
$errors = $decoded['error']['errors'];
7597
}
7698

99+
$map = null;
77100
if ($client) {
78101
$client->getLogger()->error(
79102
$err,
80103
array('code' => $code, 'errors' => $errors)
81104
);
105+
106+
$map = $client->getClassConfig(
107+
'Google_Service_Exception',
108+
'retry_map'
109+
);
82110
}
83-
throw new Google_Service_Exception($err, $code, null, $errors);
111+
throw new Google_Service_Exception($err, $code, null, $errors, $map);
84112
}
85113

86114
// Only attempt to decode the response, if the response code wasn't (204) 'no content'

src/Google/IO/Curl.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@ public function executeRequest(Google_Http_Request $request)
9090
$response = curl_exec($curl);
9191
if ($response === false) {
9292
$error = curl_error($curl);
93+
$code = curl_errno($curl);
94+
$map = $this->client->getClassConfig('Google_IO_Exception', 'retry_map');
9395

9496
$this->client->getLogger()->error('cURL ' . $error);
95-
throw new Google_IO_Exception($error);
97+
throw new Google_IO_Exception($error, $code, null, $map);
9698
}
9799
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
98100

src/Google/IO/Exception.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,51 @@
1717

1818
require_once realpath(dirname(__FILE__) . '/../../../autoload.php');
1919

20-
class Google_IO_Exception extends Google_Exception
20+
class Google_IO_Exception extends Google_Exception implements Google_Task_Retryable
2121
{
22+
/**
23+
* @var array $retryMap Map of errors with retry counts.
24+
*/
25+
private $retryMap = array();
26+
27+
/**
28+
* Creates a new IO exception with an optional retry map.
29+
*
30+
* @param string $message
31+
* @param int $code
32+
* @param Exception|null $previous
33+
* @param array|null $retryMap Map of errors with retry counts.
34+
*/
35+
public function __construct(
36+
$message,
37+
$code = 0,
38+
Exception $previous = null,
39+
array $retryMap = null
40+
) {
41+
if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
42+
parent::__construct($message, $code, $previous);
43+
} else {
44+
parent::__construct($message, $code);
45+
}
46+
47+
if (is_array($retryMap)) {
48+
$this->retryMap = $retryMap;
49+
}
50+
}
51+
52+
/**
53+
* Gets the number of times the associated task can be retried.
54+
*
55+
* NOTE: -1 is returned if the task can be retried indefinitely
56+
*
57+
* @return integer
58+
*/
59+
public function allowedRetries()
60+
{
61+
if (isset($this->retryMap[$this->code])) {
62+
return $this->retryMap[$this->code];
63+
}
64+
65+
return 0;
66+
}
2267
}

src/Google/Service/Exception.php

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,51 @@
11
<?php
2+
/*
3+
* Copyright 2014 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
217

318
require_once realpath(dirname(__FILE__) . '/../../../autoload.php');
419

5-
class Google_Service_Exception extends Google_Exception
20+
class Google_Service_Exception extends Google_Exception implements Google_Task_Retryable
621
{
722
/**
823
* Optional list of errors returned in a JSON body of an HTTP error response.
924
*/
1025
protected $errors = array();
1126

1227
/**
13-
* Override default constructor to add ability to set $errors.
28+
* @var array $retryMap Map of errors with retry counts.
29+
*/
30+
private $retryMap = array();
31+
32+
/**
33+
* Override default constructor to add the ability to set $errors and a retry
34+
* map.
1435
*
1536
* @param string $message
1637
* @param int $code
1738
* @param Exception|null $previous
1839
* @param [{string, string}] errors List of errors returned in an HTTP
1940
* response. Defaults to [].
41+
* @param array|null $retryMap Map of errors with retry counts.
2042
*/
2143
public function __construct(
2244
$message,
2345
$code = 0,
2446
Exception $previous = null,
25-
$errors = array()
47+
$errors = array(),
48+
array $retryMap = null
2649
) {
2750
if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
2851
parent::__construct($message, $code, $previous);
@@ -31,6 +54,10 @@ public function __construct(
3154
}
3255

3356
$this->errors = $errors;
57+
58+
if (is_array($retryMap)) {
59+
$this->retryMap = $retryMap;
60+
}
3461
}
3562

3663
/**
@@ -50,4 +77,27 @@ public function getErrors()
5077
{
5178
return $this->errors;
5279
}
80+
81+
/**
82+
* Gets the number of times the associated task can be retried.
83+
*
84+
* NOTE: -1 is returned if the task can be retried indefinitely
85+
*
86+
* @return integer
87+
*/
88+
public function allowedRetries()
89+
{
90+
if (isset($this->retryMap[$this->code])) {
91+
return $this->retryMap[$this->code];
92+
}
93+
94+
$errors = $this->getErrors();
95+
96+
if (!empty($errors) && isset($errors[0]['reason']) &&
97+
isset($this->retryMap[$errors[0]['reason']])) {
98+
return $this->retryMap[$errors[0]['reason']];
99+
}
100+
101+
return 0;
102+
}
53103
}

src/Google/Task/Exception.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/*
3+
* Copyright 2014 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
require_once realpath(dirname(__FILE__) . '/../../../autoload.php');
19+
20+
class Google_Task_Exception extends Google_Exception
21+
{
22+
}

src/Google/Task/Retryable.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/*
3+
* Copyright 2014 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
require_once realpath(dirname(__FILE__) . '/../../../autoload.php');
19+
20+
/**
21+
* Interface for checking how many times a given task can be retried following
22+
* a failure.
23+
*/
24+
interface Google_Task_Retryable
25+
{
26+
/**
27+
* Gets the number of times the associated task can be retried.
28+
*
29+
* NOTE: -1 is returned if the task can be retried indefinitely
30+
*
31+
* @return integer
32+
*/
33+
public function allowedRetries();
34+
}

0 commit comments

Comments
 (0)