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