vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php line 163

Open in your IDE?
  1. <?php
  2. namespace GuzzleHttp\Handler;
  3. use GuzzleHttp\Promise as P;
  4. use GuzzleHttp\Promise\Promise;
  5. use GuzzleHttp\Promise\PromiseInterface;
  6. use GuzzleHttp\Utils;
  7. use Psr\Http\Message\RequestInterface;
  8. /**
  9.  * Returns an asynchronous response using curl_multi_* functions.
  10.  *
  11.  * When using the CurlMultiHandler, custom curl options can be specified as an
  12.  * associative array of curl option constants mapping to values in the
  13.  * **curl** key of the provided request options.
  14.  *
  15.  * @final
  16.  */
  17. class CurlMultiHandler
  18. {
  19.     /**
  20.      * @var CurlFactoryInterface
  21.      */
  22.     private $factory;
  23.     /**
  24.      * @var int
  25.      */
  26.     private $selectTimeout;
  27.     /**
  28.      * @var int Will be higher than 0 when `curl_multi_exec` is still running.
  29.      */
  30.     private $active 0;
  31.     /**
  32.      * @var array Request entry handles, indexed by handle id in `addRequest`.
  33.      *
  34.      * @see CurlMultiHandler::addRequest
  35.      */
  36.     private $handles = [];
  37.     /**
  38.      * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`.
  39.      *
  40.      * @see CurlMultiHandler::addRequest
  41.      */
  42.     private $delays = [];
  43.     /**
  44.      * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt()
  45.      */
  46.     private $options = [];
  47.     /** @var resource|\CurlMultiHandle */
  48.     private $_mh;
  49.     /**
  50.      * This handler accepts the following options:
  51.      *
  52.      * - handle_factory: An optional factory  used to create curl handles
  53.      * - select_timeout: Optional timeout (in seconds) to block before timing
  54.      *   out while selecting curl handles. Defaults to 1 second.
  55.      * - options: An associative array of CURLMOPT_* options and
  56.      *   corresponding values for curl_multi_setopt()
  57.      */
  58.     public function __construct(array $options = [])
  59.     {
  60.         $this->factory $options['handle_factory'] ?? new CurlFactory(50);
  61.         if (isset($options['select_timeout'])) {
  62.             $this->selectTimeout $options['select_timeout'];
  63.         } elseif ($selectTimeout Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
  64.             @trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED);
  65.             $this->selectTimeout = (int) $selectTimeout;
  66.         } else {
  67.             $this->selectTimeout 1;
  68.         }
  69.         $this->options $options['options'] ?? [];
  70.         // unsetting the property forces the first access to go through
  71.         // __get().
  72.         unset($this->_mh);
  73.     }
  74.     /**
  75.      * @param string $name
  76.      *
  77.      * @return resource|\CurlMultiHandle
  78.      *
  79.      * @throws \BadMethodCallException when another field as `_mh` will be gotten
  80.      * @throws \RuntimeException       when curl can not initialize a multi handle
  81.      */
  82.     public function __get($name)
  83.     {
  84.         if ($name !== '_mh') {
  85.             throw new \BadMethodCallException("Can not get other property as '_mh'.");
  86.         }
  87.         $multiHandle = \curl_multi_init();
  88.         if (false === $multiHandle) {
  89.             throw new \RuntimeException('Can not initialize curl multi handle.');
  90.         }
  91.         $this->_mh $multiHandle;
  92.         foreach ($this->options as $option => $value) {
  93.             // A warning is raised in case of a wrong option.
  94.             curl_multi_setopt($this->_mh$option$value);
  95.         }
  96.         return $this->_mh;
  97.     }
  98.     public function __destruct()
  99.     {
  100.         if (isset($this->_mh)) {
  101.             \curl_multi_close($this->_mh);
  102.             unset($this->_mh);
  103.         }
  104.     }
  105.     public function __invoke(RequestInterface $request, array $options): PromiseInterface
  106.     {
  107.         $easy $this->factory->create($request$options);
  108.         $id = (int) $easy->handle;
  109.         $promise = new Promise(
  110.             [$this'execute'],
  111.             function () use ($id) {
  112.                 return $this->cancel($id);
  113.             }
  114.         );
  115.         $this->addRequest(['easy' => $easy'deferred' => $promise]);
  116.         return $promise;
  117.     }
  118.     /**
  119.      * Ticks the curl event loop.
  120.      */
  121.     public function tick(): void
  122.     {
  123.         // Add any delayed handles if needed.
  124.         if ($this->delays) {
  125.             $currentTime Utils::currentTime();
  126.             foreach ($this->delays as $id => $delay) {
  127.                 if ($currentTime >= $delay) {
  128.                     unset($this->delays[$id]);
  129.                     \curl_multi_add_handle(
  130.                         $this->_mh,
  131.                         $this->handles[$id]['easy']->handle
  132.                     );
  133.                 }
  134.             }
  135.         }
  136.         // Step through the task queue which may add additional requests.
  137.         P\Utils::queue()->run();
  138.         if ($this->active && \curl_multi_select($this->_mh$this->selectTimeout) === -1) {
  139.             // Perform a usleep if a select returns -1.
  140.             // See: https://bugs.php.net/bug.php?id=61141
  141.             \usleep(250);
  142.         }
  143.         while (\curl_multi_exec($this->_mh$this->active) === \CURLM_CALL_MULTI_PERFORM) {
  144.         }
  145.         $this->processMessages();
  146.     }
  147.     /**
  148.      * Runs until all outstanding connections have completed.
  149.      */
  150.     public function execute(): void
  151.     {
  152.         $queue P\Utils::queue();
  153.         while ($this->handles || !$queue->isEmpty()) {
  154.             // If there are no transfers, then sleep for the next delay
  155.             if (!$this->active && $this->delays) {
  156.                 \usleep($this->timeToNext());
  157.             }
  158.             $this->tick();
  159.         }
  160.     }
  161.     private function addRequest(array $entry): void
  162.     {
  163.         $easy $entry['easy'];
  164.         $id = (int) $easy->handle;
  165.         $this->handles[$id] = $entry;
  166.         if (empty($easy->options['delay'])) {
  167.             \curl_multi_add_handle($this->_mh$easy->handle);
  168.         } else {
  169.             $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
  170.         }
  171.     }
  172.     /**
  173.      * Cancels a handle from sending and removes references to it.
  174.      *
  175.      * @param int $id Handle ID to cancel and remove.
  176.      *
  177.      * @return bool True on success, false on failure.
  178.      */
  179.     private function cancel($id): bool
  180.     {
  181.         if (!is_int($id)) {
  182.             trigger_deprecation('guzzlehttp/guzzle''7.4''Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.'__CLASS____FUNCTION__);
  183.         }
  184.         // Cannot cancel if it has been processed.
  185.         if (!isset($this->handles[$id])) {
  186.             return false;
  187.         }
  188.         $handle $this->handles[$id]['easy']->handle;
  189.         unset($this->delays[$id], $this->handles[$id]);
  190.         \curl_multi_remove_handle($this->_mh$handle);
  191.         \curl_close($handle);
  192.         return true;
  193.     }
  194.     private function processMessages(): void
  195.     {
  196.         while ($done = \curl_multi_info_read($this->_mh)) {
  197.             if ($done['msg'] !== \CURLMSG_DONE) {
  198.                 // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
  199.                 continue;
  200.             }
  201.             $id = (int) $done['handle'];
  202.             \curl_multi_remove_handle($this->_mh$done['handle']);
  203.             if (!isset($this->handles[$id])) {
  204.                 // Probably was cancelled.
  205.                 continue;
  206.             }
  207.             $entry $this->handles[$id];
  208.             unset($this->handles[$id], $this->delays[$id]);
  209.             $entry['easy']->errno $done['result'];
  210.             $entry['deferred']->resolve(
  211.                 CurlFactory::finish($this$entry['easy'], $this->factory)
  212.             );
  213.         }
  214.     }
  215.     private function timeToNext(): int
  216.     {
  217.         $currentTime Utils::currentTime();
  218.         $nextTime = \PHP_INT_MAX;
  219.         foreach ($this->delays as $time) {
  220.             if ($time $nextTime) {
  221.                 $nextTime $time;
  222.             }
  223.         }
  224.         return ((int) \max(0$nextTime $currentTime)) * 1000000;
  225.     }
  226. }