PageRenderTime 25ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/iwp-client/lib/amazon/guzzle/guzzle/src/Guzzle/Http/Curl/CurlMulti.php

https://gitlab.com/treighton/wpgit
PHP | 423 lines | 309 code | 31 blank | 83 comment | 35 complexity | 9437eee15244986bc7a4a037669e96b5 MD5 | raw file
  1. <?php
  2. namespace Guzzle\Http\Curl;
  3. use Guzzle\Common\AbstractHasDispatcher;
  4. use Guzzle\Common\Event;
  5. use Guzzle\Http\Exception\MultiTransferException;
  6. use Guzzle\Http\Exception\CurlException;
  7. use Guzzle\Http\Message\RequestInterface;
  8. use Guzzle\Http\Message\EntityEnclosingRequestInterface;
  9. use Guzzle\Http\Exception\RequestException;
  10. /**
  11. * Send {@see RequestInterface} objects in parallel using curl_multi
  12. */
  13. class CurlMulti extends AbstractHasDispatcher implements CurlMultiInterface
  14. {
  15. /** @var resource cURL multi handle. */
  16. protected $multiHandle;
  17. /** @var array Attached {@see RequestInterface} objects. */
  18. protected $requests;
  19. /** @var \SplObjectStorage RequestInterface to CurlHandle hash */
  20. protected $handles;
  21. /** @var array Hash mapping curl handle resource IDs to request objects */
  22. protected $resourceHash;
  23. /** @var array Queued exceptions */
  24. protected $exceptions = array();
  25. /** @var array Requests that succeeded */
  26. protected $successful = array();
  27. /** @var array cURL multi error values and codes */
  28. protected $multiErrors = array(
  29. CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'),
  30. CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."),
  31. CURLM_OUT_OF_MEMORY => array('CURLM_OUT_OF_MEMORY', 'You are doomed.'),
  32. CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!')
  33. );
  34. /** @var float */
  35. protected $selectTimeout;
  36. public function __construct($selectTimeout = 1.0)
  37. {
  38. $this->selectTimeout = $selectTimeout;
  39. $this->multiHandle = curl_multi_init();
  40. // @codeCoverageIgnoreStart
  41. if ($this->multiHandle === false) {
  42. throw new CurlException('Unable to create multi handle');
  43. }
  44. // @codeCoverageIgnoreEnd
  45. $this->reset();
  46. }
  47. public function __destruct()
  48. {
  49. if (is_resource($this->multiHandle)) {
  50. curl_multi_close($this->multiHandle);
  51. }
  52. }
  53. public function add(RequestInterface $request)
  54. {
  55. $this->requests[] = $request;
  56. // If requests are currently transferring and this is async, then the
  57. // request must be prepared now as the send() method is not called.
  58. $this->beforeSend($request);
  59. $this->dispatch(self::ADD_REQUEST, array('request' => $request));
  60. return $this;
  61. }
  62. public function all()
  63. {
  64. return $this->requests;
  65. }
  66. public function remove(RequestInterface $request)
  67. {
  68. $this->removeHandle($request);
  69. if (($index = array_search($request, $this->requests, true)) !== false) {
  70. $request = $this->requests[$index];
  71. unset($this->requests[$index]);
  72. $this->requests = array_values($this->requests);
  73. $this->dispatch(self::REMOVE_REQUEST, array('request' => $request));
  74. return true;
  75. }
  76. return false;
  77. }
  78. public function reset($hard = false)
  79. {
  80. // Remove each request
  81. if ($this->requests) {
  82. foreach ($this->requests as $request) {
  83. $this->remove($request);
  84. }
  85. }
  86. $this->handles = new \SplObjectStorage();
  87. $this->requests = $this->resourceHash = $this->exceptions = $this->successful = array();
  88. }
  89. public function send()
  90. {
  91. $this->perform();
  92. $exceptions = $this->exceptions;
  93. $successful = $this->successful;
  94. $this->reset();
  95. if ($exceptions) {
  96. $this->throwMultiException($exceptions, $successful);
  97. }
  98. }
  99. public function count()
  100. {
  101. return count($this->requests);
  102. }
  103. /**
  104. * Build and throw a MultiTransferException
  105. *
  106. * @param array $exceptions Exceptions encountered
  107. * @param array $successful Successful requests
  108. * @throws MultiTransferException
  109. */
  110. protected function throwMultiException(array $exceptions, array $successful)
  111. {
  112. $multiException = new MultiTransferException('Errors during multi transfer');
  113. while ($e = array_shift($exceptions)) {
  114. $multiException->addFailedRequestWithException($e['request'], $e['exception']);
  115. }
  116. // Add successful requests
  117. foreach ($successful as $request) {
  118. if (!$multiException->containsRequest($request)) {
  119. $multiException->addSuccessfulRequest($request);
  120. }
  121. }
  122. throw $multiException;
  123. }
  124. /**
  125. * Prepare for sending
  126. *
  127. * @param RequestInterface $request Request to prepare
  128. * @throws \Exception on error preparing the request
  129. */
  130. protected function beforeSend(RequestInterface $request)
  131. {
  132. try {
  133. $state = $request->setState(RequestInterface::STATE_TRANSFER);
  134. if ($state == RequestInterface::STATE_TRANSFER) {
  135. $this->addHandle($request);
  136. } else {
  137. // Requests might decide they don't need to be sent just before
  138. // transfer (e.g. CachePlugin)
  139. $this->remove($request);
  140. if ($state == RequestInterface::STATE_COMPLETE) {
  141. $this->successful[] = $request;
  142. }
  143. }
  144. } catch (\Exception $e) {
  145. // Queue the exception to be thrown when sent
  146. $this->removeErroredRequest($request, $e);
  147. }
  148. }
  149. private function addHandle(RequestInterface $request)
  150. {
  151. $handle = $this->createCurlHandle($request)->getHandle();
  152. $this->checkCurlResult(
  153. curl_multi_add_handle($this->multiHandle, $handle)
  154. );
  155. }
  156. /**
  157. * Create a curl handle for a request
  158. *
  159. * @param RequestInterface $request Request
  160. *
  161. * @return CurlHandle
  162. */
  163. protected function createCurlHandle(RequestInterface $request)
  164. {
  165. $wrapper = CurlHandle::factory($request);
  166. $this->handles[$request] = $wrapper;
  167. $this->resourceHash[(int) $wrapper->getHandle()] = $request;
  168. return $wrapper;
  169. }
  170. /**
  171. * Get the data from the multi handle
  172. */
  173. protected function perform()
  174. {
  175. $event = new Event(array('curl_multi' => $this));
  176. while ($this->requests) {
  177. // Notify each request as polling
  178. $blocking = $total = 0;
  179. foreach ($this->requests as $request) {
  180. ++$total;
  181. $event['request'] = $request;
  182. $request->getEventDispatcher()->dispatch(self::POLLING_REQUEST, $event);
  183. // The blocking variable just has to be non-falsey to block the loop
  184. if ($request->getParams()->hasKey(self::BLOCKING)) {
  185. ++$blocking;
  186. }
  187. }
  188. if ($blocking == $total) {
  189. // Sleep to prevent eating CPU because no requests are actually pending a select call
  190. usleep(500);
  191. } else {
  192. $this->executeHandles();
  193. }
  194. }
  195. }
  196. /**
  197. * Execute and select curl handles
  198. */
  199. private function executeHandles()
  200. {
  201. // The first curl_multi_select often times out no matter what, but is usually required for fast transfers
  202. $selectTimeout = 0.001;
  203. $active = false;
  204. do {
  205. while (($mrc = curl_multi_exec($this->multiHandle, $active)) == CURLM_CALL_MULTI_PERFORM);
  206. $this->checkCurlResult($mrc);
  207. $this->processMessages();
  208. if ($active && curl_multi_select($this->multiHandle, $selectTimeout) === -1) {
  209. // Perform a usleep if a select returns -1: https://bugs.php.net/bug.php?id=61141
  210. usleep(150);
  211. }
  212. $selectTimeout = $this->selectTimeout;
  213. } while ($active);
  214. }
  215. /**
  216. * Process any received curl multi messages
  217. */
  218. private function processMessages()
  219. {
  220. while ($done = curl_multi_info_read($this->multiHandle)) {
  221. $request = $this->resourceHash[(int) $done['handle']];
  222. try {
  223. $this->processResponse($request, $this->handles[$request], $done);
  224. $this->successful[] = $request;
  225. } catch (\Exception $e) {
  226. $this->removeErroredRequest($request, $e);
  227. }
  228. }
  229. }
  230. /**
  231. * Remove a request that encountered an exception
  232. *
  233. * @param RequestInterface $request Request to remove
  234. * @param \Exception $e Exception encountered
  235. */
  236. protected function removeErroredRequest(RequestInterface $request, \Exception $e = null)
  237. {
  238. $this->exceptions[] = array('request' => $request, 'exception' => $e);
  239. $this->remove($request);
  240. $this->dispatch(self::MULTI_EXCEPTION, array('exception' => $e, 'all_exceptions' => $this->exceptions));
  241. }
  242. /**
  243. * Check for errors and fix headers of a request based on a curl response
  244. *
  245. * @param RequestInterface $request Request to process
  246. * @param CurlHandle $handle Curl handle object
  247. * @param array $curl Array returned from curl_multi_info_read
  248. *
  249. * @throws CurlException on Curl error
  250. */
  251. protected function processResponse(RequestInterface $request, CurlHandle $handle, array $curl)
  252. {
  253. // Set the transfer stats on the response
  254. $handle->updateRequestFromTransfer($request);
  255. // Check if a cURL exception occurred, and if so, notify things
  256. $curlException = $this->isCurlException($request, $handle, $curl);
  257. // Always remove completed curl handles. They can be added back again
  258. // via events if needed (e.g. ExponentialBackoffPlugin)
  259. $this->removeHandle($request);
  260. if (!$curlException) {
  261. if ($this->validateResponseWasSet($request)) {
  262. $state = $request->setState(
  263. RequestInterface::STATE_COMPLETE,
  264. array('handle' => $handle)
  265. );
  266. // Only remove the request if it wasn't resent as a result of
  267. // the state change
  268. if ($state != RequestInterface::STATE_TRANSFER) {
  269. $this->remove($request);
  270. }
  271. }
  272. return;
  273. }
  274. // Set the state of the request to an error
  275. $state = $request->setState(RequestInterface::STATE_ERROR, array('exception' => $curlException));
  276. // Allow things to ignore the error if possible
  277. if ($state != RequestInterface::STATE_TRANSFER) {
  278. $this->remove($request);
  279. }
  280. // The error was not handled, so fail
  281. if ($state == RequestInterface::STATE_ERROR) {
  282. /** @var CurlException $curlException */
  283. throw $curlException;
  284. }
  285. }
  286. /**
  287. * Remove a curl handle from the curl multi object
  288. *
  289. * @param RequestInterface $request Request that owns the handle
  290. */
  291. protected function removeHandle(RequestInterface $request)
  292. {
  293. if (isset($this->handles[$request])) {
  294. $handle = $this->handles[$request];
  295. curl_multi_remove_handle($this->multiHandle, $handle->getHandle());
  296. unset($this->handles[$request]);
  297. unset($this->resourceHash[(int) $handle->getHandle()]);
  298. $handle->close();
  299. }
  300. }
  301. /**
  302. * Check if a cURL transfer resulted in what should be an exception
  303. *
  304. * @param RequestInterface $request Request to check
  305. * @param CurlHandle $handle Curl handle object
  306. * @param array $curl Array returned from curl_multi_info_read
  307. *
  308. * @return CurlException|bool
  309. */
  310. private function isCurlException(RequestInterface $request, CurlHandle $handle, array $curl)
  311. {
  312. if (CURLM_OK == $curl['result'] || CURLM_CALL_MULTI_PERFORM == $curl['result']) {
  313. return false;
  314. }
  315. $handle->setErrorNo($curl['result']);
  316. $e = new CurlException(sprintf('[curl] %s: %s [url] %s',
  317. $handle->getErrorNo(), $handle->getError(), $handle->getUrl()));
  318. $e->setCurlHandle($handle)
  319. ->setRequest($request)
  320. ->setCurlInfo($handle->getInfo())
  321. ->setError($handle->getError(), $handle->getErrorNo());
  322. return $e;
  323. }
  324. /**
  325. * Throw an exception for a cURL multi response if needed
  326. *
  327. * @param int $code Curl response code
  328. * @throws CurlException
  329. */
  330. private function checkCurlResult($code)
  331. {
  332. if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) {
  333. throw new CurlException(isset($this->multiErrors[$code])
  334. ? "cURL error: {$code} ({$this->multiErrors[$code][0]}): cURL message: {$this->multiErrors[$code][1]}"
  335. : 'Unexpected cURL error: ' . $code
  336. );
  337. }
  338. }
  339. /**
  340. * @link https://github.com/guzzle/guzzle/issues/710
  341. */
  342. private function validateResponseWasSet(RequestInterface $request)
  343. {
  344. if ($request->getResponse()) {
  345. return true;
  346. }
  347. $body = $request instanceof EntityEnclosingRequestInterface
  348. ? $request->getBody()
  349. : null;
  350. if (!$body) {
  351. $rex = new RequestException(
  352. 'No response was received for a request with no body. This'
  353. . ' could mean that you are saturating your network.'
  354. );
  355. $rex->setRequest($request);
  356. $this->removeErroredRequest($request, $rex);
  357. } elseif (!$body->isSeekable() || !$body->seek(0)) {
  358. // Nothing we can do with this. Sorry!
  359. $rex = new RequestException(
  360. 'The connection was unexpectedly closed. The request would'
  361. . ' have been retried, but attempting to rewind the'
  362. . ' request body failed.'
  363. );
  364. $rex->setRequest($request);
  365. $this->removeErroredRequest($request, $rex);
  366. } else {
  367. $this->remove($request);
  368. // Add the request back to the batch to retry automatically.
  369. $this->requests[] = $request;
  370. $this->addHandle($request);
  371. }
  372. return false;
  373. }
  374. }