-
Notifications
You must be signed in to change notification settings - Fork 853
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
348 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,9 +51,10 @@ public static function instance() | |
* | ||
* @param array|callable|null $defaultOptions | ||
*/ | ||
public function __construct($defaultOptions = null) | ||
public function __construct($defaultOptions = null, $randomGenerator = null) | ||
{ | ||
$this->defaultOptions = $defaultOptions; | ||
$this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator(); | ||
$this->initUserAgentInfo(); | ||
} | ||
|
||
|
@@ -110,7 +111,6 @@ public function getConnectTimeout() | |
|
||
public function request($method, $absUrl, $headers, $params, $hasFile) | ||
{ | ||
$curl = curl_init(); | ||
$method = strtolower($method); | ||
|
||
$opts = []; | ||
|
@@ -147,6 +147,14 @@ public function request($method, $absUrl, $headers, $params, $hasFile) | |
throw new Error\Api("Unrecognized method $method"); | ||
} | ||
|
||
// It is only safe to retry network failures on POST requests if we | ||
// add an Idempotency-Key header | ||
if (($method == 'post') && (Stripe::$maxNetworkRetries > 0)) { | ||
if (!isset($headers['Idempotency-Key'])) { | ||
array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator->uuid()); | ||
} | ||
} | ||
|
||
// Create a callback to capture HTTP headers for the response | ||
$rheaders = []; | ||
$headerCallback = function ($curl, $header_line) use (&$rheaders) { | ||
|
@@ -185,27 +193,58 @@ public function request($method, $absUrl, $headers, $params, $hasFile) | |
$opts[CURLOPT_SSL_VERIFYPEER] = false; | ||
} | ||
|
||
curl_setopt_array($curl, $opts); | ||
$rbody = curl_exec($curl); | ||
list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl); | ||
|
||
if ($rbody === false) { | ||
$errno = curl_errno($curl); | ||
$message = curl_error($curl); | ||
return [$rbody, $rcode, $rheaders]; | ||
} | ||
|
||
/** | ||
* @param array $opts cURL options | ||
*/ | ||
private function executeRequestWithRetries($opts, $absUrl) | ||
{ | ||
$numRetries = 0; | ||
|
||
while (true) { | ||
$rcode = 0; | ||
$errno = 0; | ||
|
||
$curl = curl_init(); | ||
curl_setopt_array($curl, $opts); | ||
$rbody = curl_exec($curl); | ||
|
||
if ($rbody === false) { | ||
$errno = curl_errno($curl); | ||
$message = curl_error($curl); | ||
} else { | ||
$rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); | ||
} | ||
curl_close($curl); | ||
$this->handleCurlError($absUrl, $errno, $message); | ||
|
||
if ($this->shouldRetry($errno, $rcode, $numRetries)) { | ||
$numRetries += 1; | ||
$sleepSeconds = $this->sleepTime($numRetries); | ||
usleep(intval($sleepSeconds * 1000000)); | ||
} else { | ||
break; | ||
} | ||
} | ||
|
||
$rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); | ||
curl_close($curl); | ||
return [$rbody, $rcode, $rheaders]; | ||
if ($rbody === false) { | ||
$this->handleCurlError($absUrl, $errno, $message, $numRetries); | ||
} | ||
|
||
return [$rbody, $rcode]; | ||
} | ||
|
||
/** | ||
* @param string $url | ||
* @param number $errno | ||
* @param string $message | ||
* @param int $numRetries | ||
* @throws Error\ApiConnection | ||
*/ | ||
private function handleCurlError($url, $errno, $message) | ||
private function handleCurlError($url, $errno, $message, $numRetries) | ||
{ | ||
switch ($errno) { | ||
case CURLE_COULDNT_CONNECT: | ||
|
@@ -230,6 +269,66 @@ private function handleCurlError($url, $errno, $message) | |
$msg .= " let us know at [email protected]."; | ||
|
||
$msg .= "\n\n(Network error [errno $errno]: $message)"; | ||
|
||
if ($numRetries > 0) { | ||
$msg .= "\n\nRequest was retried $numRetries times."; | ||
} | ||
|
||
throw new Error\ApiConnection($msg); | ||
} | ||
|
||
/** | ||
* Checks if an error is a problem that we should retry on. This includes both | ||
* socket errors that may represent an intermittent problem and some special | ||
* HTTP statuses. | ||
* @param int $errno | ||
* @param int $rcode | ||
* @param int $numRetries | ||
* @return bool | ||
*/ | ||
private function shouldRetry($errno, $rcode, $numRetries) | ||
{ | ||
if ($numRetries >= Stripe::getMaxNetworkRetries()) { | ||
return false; | ||
} | ||
|
||
// Retry on timeout-related problems (either on open or read). | ||
if ($errno === CURLE_OPERATION_TIMEOUTED) { | ||
return true; | ||
} | ||
|
||
// Destination refused the connection, the connection was reset, or a | ||
// variety of other connection failures. This could occur from a single | ||
// saturated server, so retry in case it's intermittent. | ||
if ($errno === CURLE_COULDNT_CONNECT) { | ||
return true; | ||
} | ||
|
||
// 409 conflict | ||
if ($rcode === 409) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private function sleepTime($numRetries) | ||
{ | ||
// Apply exponential backoff with $initialNetworkRetryDelay on the | ||
// number of $numRetries so far as inputs. Do not allow the number to exceed | ||
// $maxNetworkRetryDelay. | ||
$sleepSeconds = min( | ||
Stripe::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1), | ||
Stripe::getMaxNetworkRetryDelay() | ||
); | ||
|
||
// Apply some jitter by randomizing the value in the range of | ||
// ($sleepSeconds / 2) to ($sleepSeconds). | ||
$sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat()); | ||
|
||
// But never sleep less than the base sleep seconds. | ||
$sleepSeconds = max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds); | ||
|
||
return $sleepSeconds; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
namespace Stripe\Util; | ||
|
||
class RandomGenerator | ||
{ | ||
/** | ||
* Returns a random value between 0 and $max. | ||
* | ||
* @param float $max (optional) | ||
* @return float | ||
*/ | ||
public function randFloat($max = 1.0) | ||
{ | ||
return mt_rand() / mt_getrandmax() * $max; | ||
} | ||
|
||
/** | ||
* Returns a v4 UUID. | ||
* | ||
* @return string | ||
*/ | ||
public function uuid() | ||
{ | ||
$arr = array_values(unpack('N1a/n4b/N1c', openssl_random_pseudo_bytes(16))); | ||
$arr[2] = ($arr[2] & 0x0fff) | 0x4000; | ||
$arr[3] = ($arr[3] & 0x3fff) | 0x8000; | ||
return vsprintf('%08x-%04x-%04x-%04x-%04x%08x', $arr); | ||
} | ||
} |
Oops, something went wrong.