diff --git a/README.md b/README.md index ac44c20..4382d9f 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ MultiCurl::setOpts($options) MultiCurl::setPort($port) MultiCurl::setReferer($referer) MultiCurl::setReferrer($referrer) +MultiCurl::setRetry($maximum_number_of_retries = 0) MultiCurl::setTimeout($seconds) MultiCurl::setUrl($url) MultiCurl::setUserAgent($user_agent) diff --git a/src/Curl/Curl.php b/src/Curl/Curl.php index e3a4b9c..5a5ea54 100644 --- a/src/Curl/Curl.php +++ b/src/Curl/Curl.php @@ -43,8 +43,9 @@ class Curl public $attempts = 0; public $retries = 0; + public $isChildOfMultiCurl = false; + public $remainingRetries = 0; private $maximumNumberOfRetries = 0; - private $remainingRetries = 0; private $cookies = array(); private $headers = array(); @@ -390,8 +391,14 @@ class Curl $this->errorMessage = $this->curlError ? $this->curlErrorMessage : $this->httpErrorMessage; if ($this->error && $this->remainingRetries >= 1) { - $this->remainingRetries -= 1; $this->retries += 1; + + // Allow multicurl to update $remainingRetries and retry. + if ($this->isChildOfMultiCurl) { + return; + } + + $this->remainingRetries -= 1; return $this->exec($ch); } diff --git a/src/Curl/MultiCurl.php b/src/Curl/MultiCurl.php index 7e2297b..d2e3540 100644 --- a/src/Curl/MultiCurl.php +++ b/src/Curl/MultiCurl.php @@ -18,6 +18,8 @@ class MultiCurl private $errorFunction = null; private $completeFunction = null; + private $maximumNumberOfRetries = 0; + private $cookies = array(); private $headers = array(); private $options = array(); @@ -587,6 +589,19 @@ class MultiCurl $this->setOpt(CURLOPT_REFERER, $referrer); } + /** + * Set Retry + * + * Number of retries to attempt. Maximum number of attempts is $maximum_number_of_retries + 1. + * + * @access public + * @param $maximum_number_of_retries + */ + public function setRetry($maximum_number_of_retries = 0) + { + $this->maximumNumberOfRetries = $maximum_number_of_retries; + } + /** * Set Timeout * @@ -661,17 +676,31 @@ class MultiCurl $ch->curlErrorCode = $info_array['result']; $ch->exec($ch->curl); - // Remove completed instance from active curls. - unset($this->activeCurls[$key]); + if ($ch->error && $ch->remainingRetries >= 1) { + $ch->remainingRetries -= 1; - // Start a new request before removing the handle of the completed one. - if (count($this->curls) >= 1) { - $this->initHandle(array_shift($this->curls)); + // Remove completed handle before adding again in order to retry request. + curl_multi_remove_handle($this->multiCurl, $ch->curl); + + $curlm_error_code = curl_multi_add_handle($this->multiCurl, $ch->curl); + if (!($curlm_error_code === CURLM_OK)) { + throw new \ErrorException( + 'cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code) + ); + } + } else { + // Remove completed instance from active curls. + unset($this->activeCurls[$key]); + + // Start a new request before removing the handle of the completed one. + if (count($this->curls) >= 1) { + $this->initHandle(array_shift($this->curls)); + } + curl_multi_remove_handle($this->multiCurl, $ch->curl); + + // Clean up completed instance. + $ch->close(); } - curl_multi_remove_handle($this->multiCurl, $ch->curl); - - // Clean up completed instance. - $ch->close(); break; } @@ -763,6 +792,7 @@ class MultiCurl { // Use sequential ids to allow for ordered post processing. $curl->id = $this->nextCurlId++; + $curl->isChildOfMultiCurl = true; $this->curls[$curl->id] = $curl; } @@ -791,6 +821,7 @@ class MultiCurl $curl->setOpts($this->options); $curl->setHeaders($this->headers); + $curl->setRetry($this->maximumNumberOfRetries); foreach ($this->cookies as $key => $value) { $curl->setCookie($key, $value); diff --git a/tests/PHPCurlClass/PHPCurlClassTest.php b/tests/PHPCurlClass/PHPCurlClassTest.php index 202ca02..d533c84 100644 --- a/tests/PHPCurlClass/PHPCurlClassTest.php +++ b/tests/PHPCurlClass/PHPCurlClassTest.php @@ -3293,6 +3293,13 @@ class CurlTest extends \PHPUnit\Framework\TestCase 'expect_attempts' => 3, 'expect_retries' => 2, ), + array( + 'maximum_number_of_retries' => 3, + 'failures' => 3, + 'expect_success' => true, + 'expect_attempts' => 4, + 'expect_retries' => 3, + ), ); foreach ($tests as $test) { $maximum_number_of_retries = $test['maximum_number_of_retries']; diff --git a/tests/PHPCurlClass/PHPMultiCurlClassTest.php b/tests/PHPCurlClass/PHPMultiCurlClassTest.php index b4ac8f0..4f0995c 100644 --- a/tests/PHPCurlClass/PHPMultiCurlClassTest.php +++ b/tests/PHPCurlClass/PHPMultiCurlClassTest.php @@ -2590,4 +2590,81 @@ class MultiCurlTest extends \PHPUnit\Framework\TestCase $multi_curl->start(); } + + public function testRetryMulti() + { + $tests = array( + array( + 'maximum_number_of_retries' => null, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ), + array( + 'maximum_number_of_retries' => 0, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ), + array( + 'maximum_number_of_retries' => 0, + 'failures' => 1, + 'expect_success' => false, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ), + array( + 'maximum_number_of_retries' => 1, + 'failures' => 1, + 'expect_success' => true, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ), + array( + 'maximum_number_of_retries' => 1, + 'failures' => 2, + 'expect_success' => false, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ), + array( + 'maximum_number_of_retries' => 2, + 'failures' => 2, + 'expect_success' => true, + 'expect_attempts' => 3, + 'expect_retries' => 2, + ), + array( + 'maximum_number_of_retries' => 3, + 'failures' => 3, + 'expect_success' => true, + 'expect_attempts' => 4, + 'expect_retries' => 3, + ), + ); + foreach ($tests as $test) { + $maximum_number_of_retries = $test['maximum_number_of_retries']; + $failures = $test['failures']; + $expect_success = $test['expect_success']; + $expect_attempts = $test['expect_attempts']; + $expect_retries = $test['expect_retries']; + + $multi_curl = new MultiCurl(); + $multi_curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $multi_curl->setHeader('X-DEBUG-TEST', 'retry'); + + if (!($maximum_number_of_retries === null)) { + $multi_curl->setRetry($maximum_number_of_retries); + } + + $instance = $multi_curl->addGet(Test::TEST_URL, array('failures' => $failures)); + $multi_curl->start(); + + $this->assertEquals($expect_success, !$instance->error); + $this->assertEquals($expect_attempts, $instance->attempts); + $this->assertEquals($expect_retries, $instance->retries); + } + } }