PluginProbe
Plausible Analytics / trunk
Plausible Analytics vtrunk
2.6.1 2.6.0 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 52 releases
plausible-analytics / src / Client / lib / Lib / GuzzleHttp / Handler / CurlMultiHandler.php

CurlMultiHandler.php in Plausible Analytics trunk, at src/Client/lib/Lib/GuzzleHttp/Handler/CurlMultiHandler.php

268 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Handler;
4
5 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Promise as P;
6 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Promise\Promise;
7 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Promise\PromiseInterface;
8 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Utils;
9 use Plausible\Analytics\WP\Client\Lib\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 * @final
19 */
20 class CurlMultiHandler
21 {
22 /**
23 * @var CurlFactoryInterface
24 */
25 private $factory;
26
27 /**
28 * @var int
29 */
30 private $selectTimeout;
31
32 /**
33 * @var int Will be higher than 0 when `curl_multi_exec` is still running.
34 */
35 private $active = 0;
36
37 /**
38 * @var array Request entry handles, indexed by handle id in `addRequest`.
39 *
40 * @see CurlMultiHandler::addRequest
41 */
42 private $handles = [];
43
44 /**
45 * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`.
46 *
47 * @see CurlMultiHandler::addRequest
48 */
49 private $delays = [];
50
51 /**
52 * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt()
53 */
54 private $options = [];
55
56 /** @var resource|\CurlMultiHandle */
57 private $_mh;
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 Plausible\Analytics\WP\Client\Lib\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 // unsetting the property forces the first access to go through
84 // __get().
85 unset($this->_mh);
86 }
87
88 /**
89 * @param string $name
90 *
91 * @return resource|\CurlMultiHandle
92 *
93 * @throws \BadMethodCallException when another field as `_mh` will be gotten
94 * @throws \RuntimeException when curl can not initialize a multi handle
95 */
96 public function __get($name)
97 {
98 if ($name !== '_mh') {
99 throw new \BadMethodCallException("Can not get other property as '_mh'.");
100 }
101
102 $multiHandle = \curl_multi_init();
103
104 if (false === $multiHandle) {
105 throw new \RuntimeException('Can not initialize curl multi handle.');
106 }
107
108 $this->_mh = $multiHandle;
109
110 foreach ($this->options as $option => $value) {
111 // A warning is raised in case of a wrong option.
112 curl_multi_setopt($this->_mh, $option, $value);
113 }
114
115 return $this->_mh;
116 }
117
118 public function __destruct()
119 {
120 if (isset($this->_mh)) {
121 \curl_multi_close($this->_mh);
122 unset($this->_mh);
123 }
124 }
125
126 public function __invoke(RequestInterface $request, array $options): PromiseInterface
127 {
128 $easy = $this->factory->create($request, $options);
129 $id = (int) $easy->handle;
130
131 $promise = new Promise(
132 [$this, 'execute'],
133 function () use ($id) {
134 return $this->cancel($id);
135 }
136 );
137
138 $this->addRequest(['easy' => $easy, 'deferred' => $promise]);
139
140 return $promise;
141 }
142
143 /**
144 * Ticks the curl event loop.
145 */
146 public function tick(): void
147 {
148 // Add any delayed handles if needed.
149 if ($this->delays) {
150 $currentTime = Utils::currentTime();
151 foreach ($this->delays as $id => $delay) {
152 if ($currentTime >= $delay) {
153 unset($this->delays[$id]);
154 \curl_multi_add_handle(
155 $this->_mh,
156 $this->handles[$id]['easy']->handle
157 );
158 }
159 }
160 }
161
162 // Step through the task queue which may add additional requests.
163 P\Utils::queue()->run();
164
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
171 while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) {
172 }
173
174 $this->processMessages();
175 }
176
177 /**
178 * Runs until all outstanding connections have completed.
179 */
180 public function execute(): void
181 {
182 $queue = P\Utils::queue();
183
184 while ($this->handles || !$queue->isEmpty()) {
185 // If there are no transfers, then sleep for the next delay
186 if (!$this->active && $this->delays) {
187 \usleep($this->timeToNext());
188 }
189 $this->tick();
190 }
191 }
192
193 private function addRequest(array $entry): void
194 {
195 $easy = $entry['easy'];
196 $id = (int) $easy->handle;
197 $this->handles[$id] = $entry;
198 if (empty($easy->options['delay'])) {
199 \curl_multi_add_handle($this->_mh, $easy->handle);
200 } else {
201 $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
202 }
203 }
204
205 /**
206 * Cancels a handle from sending and removes references to it.
207 *
208 * @param int $id Handle ID to cancel and remove.
209 *
210 * @return bool True on success, false on failure.
211 */
212 private function cancel($id): bool
213 {
214 if (!is_int($id)) {
215 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__);
216 }
217
218 // Cannot cancel if it has been processed.
219 if (!isset($this->handles[$id])) {
220 return false;
221 }
222
223 $handle = $this->handles[$id]['easy']->handle;
224 unset($this->delays[$id], $this->handles[$id]);
225 \curl_multi_remove_handle($this->_mh, $handle);
226 \curl_close($handle);
227
228 return true;
229 }
230
231 private function processMessages(): void
232 {
233 while ($done = \curl_multi_info_read($this->_mh)) {
234 if ($done['msg'] !== \CURLMSG_DONE) {
235 // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
236 continue;
237 }
238 $id = (int) $done['handle'];
239 \curl_multi_remove_handle($this->_mh, $done['handle']);
240
241 if (!isset($this->handles[$id])) {
242 // Probably was cancelled.
243 continue;
244 }
245
246 $entry = $this->handles[$id];
247 unset($this->handles[$id], $this->delays[$id]);
248 $entry['easy']->errno = $done['result'];
249 $entry['deferred']->resolve(
250 CurlFactory::finish($this, $entry['easy'], $this->factory)
251 );
252 }
253 }
254
255 private function timeToNext(): int
256 {
257 $currentTime = Utils::currentTime();
258 $nextTime = \PHP_INT_MAX;
259 foreach ($this->delays as $time) {
260 if ($time < $nextTime) {
261 $nextTime = $time;
262 }
263 }
264
265 return ((int) \max(0, $nextTime - $currentTime)) * 1000000;
266 }
267 }
268