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