PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.1
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / vendor_prefixed / guzzlehttp / guzzle / src / Handler / CurlMultiHandler.php

CurlMultiHandler.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.1, at vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php

314 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace YoastSEO_Vendor\GuzzleHttp\Handler;
4
5 use Closure;
6 use YoastSEO_Vendor\GuzzleHttp\Promise as P;
7 use YoastSEO_Vendor\GuzzleHttp\Promise\Promise;
8 use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface;
9 use YoastSEO_Vendor\GuzzleHttp\TransportSharing;
10 use YoastSEO_Vendor\GuzzleHttp\Utils;
11 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
12 /**
13 * Returns an asynchronous response using curl_multi_* functions.
14 *
15 * When using the CurlMultiHandler, custom curl options can be specified as an
16 * associative array of curl option constants mapping to values in the
17 * **curl** key of the provided request options.
18 *
19 * @final
20 */
21 class CurlMultiHandler
22 {
23 /**
24 * @var CurlFactoryInterface
25 */
26 private $factory;
27 /**
28 * @var CurlShareHandleState|null
29 */
30 private $shareHandleState;
31 /**
32 * @var int
33 */
34 private $selectTimeout;
35 /**
36 * @var int Will be higher than 0 when `curl_multi_exec` is still running.
37 */
38 private $active = 0;
39 /**
40 * @var array Request entry handles, indexed by handle id in `addRequest`.
41 *
42 * @see CurlMultiHandler::addRequest
43 */
44 private $handles = [];
45 /**
46 * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`.
47 *
48 * @see CurlMultiHandler::addRequest
49 */
50 private $delays = [];
51 /**
52 * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt()
53 */
54 private $options = [];
55 /** @var resource|\CurlMultiHandle */
56 private $_mh;
57 /**
58 * @var bool
59 */
60 private $executingMulti = \false;
61 /**
62 * @var array<int, EasyHandle>
63 */
64 private $deferredCancels = [];
65 /**
66 * This handler accepts the following options:
67 *
68 * - handle_factory: An optional factory used to create curl handles
69 * - transport_sharing: Optional transport sharing mode.
70 * - select_timeout: Optional timeout (in seconds) to block before timing
71 * out while selecting curl handles. Defaults to 1 second.
72 * - options: An associative array of CURLMOPT_* options and
73 * corresponding values for curl_multi_setopt()
74 */
75 public function __construct(array $options = [])
76 {
77 \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlMultiHandler');
78 $transportSharing = $options['transport_sharing'] ?? null;
79 $sharingMode = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
80 if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
81 $this->shareHandleState = null;
82 $this->factory = $options['handle_factory'];
83 } else {
84 $this->shareHandleState = $sharingMode !== \YoastSEO_Vendor\GuzzleHttp\TransportSharing::NONE ? \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::fromOption($transportSharing) : null;
85 $this->factory = $this->shareHandleState !== null ? new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory(50, $this->shareHandleState->mode, $this->shareHandleState->handle) : new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory(50);
86 }
87 if (isset($options['select_timeout'])) {
88 $this->selectTimeout = $options['select_timeout'];
89 } elseif ($selectTimeout = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
90 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.2', 'The GUZZLE_CURL_SELECT_TIMEOUT environment variable is deprecated; use the "select_timeout" option instead.');
91 $this->selectTimeout = (int) $selectTimeout;
92 } else {
93 $this->selectTimeout = 1;
94 }
95 $this->options = $options['options'] ?? [];
96 // unsetting the property forces the first access to go through
97 // __get().
98 unset($this->_mh);
99 }
100 /**
101 * @param string $name
102 *
103 * @return resource|\CurlMultiHandle
104 *
105 * @throws \BadMethodCallException when another field as `_mh` will be gotten
106 * @throws \RuntimeException when curl can not initialize a multi handle
107 */
108 public function __get($name)
109 {
110 if ($name !== '_mh') {
111 throw new \BadMethodCallException("Can not get other property as '_mh'.");
112 }
113 $multiHandle = \curl_multi_init();
114 if (\false === $multiHandle) {
115 throw new \RuntimeException('Can not initialize curl multi handle.');
116 }
117 $this->_mh = $multiHandle;
118 foreach ($this->options as $option => $value) {
119 // A warning is raised in case of a wrong option.
120 \curl_multi_setopt($this->_mh, $option, $value);
121 }
122 return $this->_mh;
123 }
124 public function __destruct()
125 {
126 if (isset($this->_mh)) {
127 try {
128 \curl_multi_close($this->_mh);
129 } catch (\Throwable $e) {
130 // Destructors must not throw.
131 } finally {
132 unset($this->_mh);
133 }
134 }
135 }
136 public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
137 {
138 $easy = $this->factory->create($request, $options);
139 $id = (int) $easy->handle;
140 $promise = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise([$this, 'execute'], function () use($id) {
141 return $this->cancel($id);
142 });
143 $this->addRequest(['easy' => $easy, 'deferred' => $promise]);
144 return $promise;
145 }
146 /**
147 * Ticks the curl event loop.
148 */
149 public function tick() : void
150 {
151 // Add any delayed handles if needed.
152 if ($this->delays) {
153 $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime();
154 foreach ($this->delays as $id => $delay) {
155 if ($currentTime >= $delay) {
156 unset($this->delays[$id]);
157 \curl_multi_add_handle($this->_mh, $this->handles[$id]['easy']->handle);
158 }
159 }
160 }
161 // Run curl_multi_exec in the queue to enable other async tasks to run
162 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue']));
163 // Step through the task queue which may add additional requests.
164 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->run();
165 if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) {
166 // Perform a usleep if a select returns -1.
167 // See: https://bugs.php.net/bug.php?id=61141
168 \usleep(250);
169 }
170 do {
171 $this->executingMulti = \true;
172 try {
173 $exec = \curl_multi_exec($this->_mh, $this->active);
174 } finally {
175 $this->executingMulti = \false;
176 $this->cleanupDeferredCancels();
177 }
178 // Prevent busy looping for slow HTTP requests.
179 if ($exec === \CURLM_CALL_MULTI_PERFORM) {
180 \curl_multi_select($this->_mh, $this->selectTimeout);
181 }
182 } while ($exec === \CURLM_CALL_MULTI_PERFORM);
183 $this->processMessages();
184 }
185 /**
186 * Runs \curl_multi_exec() inside the event loop, to prevent busy looping
187 */
188 private function tickInQueue() : void
189 {
190 $this->executingMulti = \true;
191 try {
192 $exec = \curl_multi_exec($this->_mh, $this->active);
193 } finally {
194 $this->executingMulti = \false;
195 $this->cleanupDeferredCancels();
196 }
197 if ($exec === \CURLM_CALL_MULTI_PERFORM) {
198 \curl_multi_select($this->_mh, 0);
199 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue']));
200 }
201 }
202 /**
203 * Runs until all outstanding connections have completed.
204 */
205 public function execute() : void
206 {
207 $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue();
208 while ($this->handles || !$queue->isEmpty()) {
209 // If there are no transfers, then sleep for the next delay
210 if (!$this->active && $this->delays) {
211 \usleep($this->timeToNext());
212 }
213 $this->tick();
214 }
215 }
216 private function addRequest(array $entry) : void
217 {
218 $easy = $entry['easy'];
219 $id = (int) $easy->handle;
220 $this->handles[$id] = $entry;
221 if (empty($easy->options['delay'])) {
222 \curl_multi_add_handle($this->_mh, $easy->handle);
223 } else {
224 $this->delays[$id] = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() + $easy->options['delay'] / 1000;
225 }
226 }
227 /**
228 * Cancels a handle from sending and removes references to it.
229 *
230 * @param int $id Handle ID to cancel and remove.
231 *
232 * @return bool True on success, false on failure.
233 */
234 private function cancel($id) : bool
235 {
236 if (!\is_int($id)) {
237 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
238 }
239 // Cannot cancel if it has been processed.
240 if (!isset($this->handles[$id])) {
241 return \false;
242 }
243 $easy = $this->handles[$id]['easy'];
244 unset($this->delays[$id], $this->handles[$id]);
245 if ($this->executingMulti) {
246 $this->deferredCancels[$id] = $easy;
247 return \true;
248 }
249 $this->cleanupCancelledHandle($easy);
250 return \true;
251 }
252 private function cleanupDeferredCancels() : void
253 {
254 if ($this->deferredCancels === []) {
255 return;
256 }
257 $entries = $this->deferredCancels;
258 $this->deferredCancels = [];
259 foreach ($entries as $easy) {
260 $this->cleanupCancelledHandle($easy);
261 }
262 }
263 private function cleanupCancelledHandle(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
264 {
265 $handle = $easy->handle;
266 \curl_multi_remove_handle($this->_mh, $handle);
267 if (\PHP_VERSION_ID < 80000) {
268 \curl_close($handle);
269 }
270 }
271 private function processMessages() : void
272 {
273 while ($done = \curl_multi_info_read($this->_mh)) {
274 if ($done['msg'] !== \CURLMSG_DONE) {
275 // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
276 continue;
277 }
278 if (!isset($done['handle'])) {
279 // Work around a PHP issue where cancelled transfers may omit the handle.
280 // Remove this once we no longer support PHP versions before the fix in
281 // https://github.com/php/php-src/pull/16302.
282 continue;
283 }
284 $id = (int) $done['handle'];
285 \curl_multi_remove_handle($this->_mh, $done['handle']);
286 if (!isset($this->handles[$id])) {
287 // Probably was cancelled.
288 continue;
289 }
290 $entry = $this->handles[$id];
291 unset($this->handles[$id], $this->delays[$id]);
292 $entry['easy']->errno = $done['result'];
293 try {
294 $result = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory::finish($this, $entry['easy'], $this->factory);
295 } catch (\Throwable $e) {
296 $entry['deferred']->reject($e);
297 continue;
298 }
299 $entry['deferred']->resolve($result);
300 }
301 }
302 private function timeToNext() : int
303 {
304 $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime();
305 $nextTime = \PHP_INT_MAX;
306 foreach ($this->delays as $time) {
307 if ($time < $nextTime) {
308 $nextTime = $time;
309 }
310 }
311 return (int) \max(0, $nextTime - $currentTime) * 1000000;
312 }
313 }
314