Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
24.31% |
35 / 144 |
|
9.52% |
2 / 21 |
CRAP | |
0.00% |
0 / 1 |
Select | |
24.31% |
35 / 144 |
|
9.52% |
2 / 21 |
1784.37 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
delay | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
repeat | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
offDelay | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
offRepeat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onReadable | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
4.25 | |||
offReadable | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
onWritable | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
offWritable | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
onExcept | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
offExcept | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onSignal | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
offSignal | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
tick | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
setNextTickTime | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
deleteAllTimer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
run | |
57.69% |
15 / 26 |
|
0.00% |
0 / 1 |
35.39 | |||
stop | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getTimerCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setErrorHandler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
safeCall | |
20.00% |
1 / 5 |
|
0.00% |
0 / 1 |
7.61 |
1 | <?php |
2 | /** |
3 | * This file is part of workerman. |
4 | * |
5 | * Licensed under The MIT License |
6 | * For full copyright and license information, please see the MIT-LICENSE.txt |
7 | * Redistributions of files must retain the above copyright notice. |
8 | * |
9 | * @author walkor<walkor@workerman.net> |
10 | * @copyright walkor<walkor@workerman.net> |
11 | * @link http://www.workerman.net/ |
12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License |
13 | */ |
14 | |
15 | declare(strict_types=1); |
16 | |
17 | namespace Workerman\Events; |
18 | |
19 | use SplPriorityQueue; |
20 | use Throwable; |
21 | use function count; |
22 | use function max; |
23 | use function microtime; |
24 | use function pcntl_signal; |
25 | use function pcntl_signal_dispatch; |
26 | use const DIRECTORY_SEPARATOR; |
27 | |
28 | /** |
29 | * select eventloop |
30 | */ |
31 | final class Select implements EventInterface |
32 | { |
33 | /** |
34 | * Running. |
35 | * |
36 | * @var bool |
37 | */ |
38 | private bool $running = true; |
39 | |
40 | /** |
41 | * All listeners for read/write event. |
42 | * |
43 | * @var array<int, callable> |
44 | */ |
45 | private array $readEvents = []; |
46 | |
47 | /** |
48 | * All listeners for read/write event. |
49 | * |
50 | * @var array<int, callable> |
51 | */ |
52 | private array $writeEvents = []; |
53 | |
54 | /** |
55 | * @var array<int, callable> |
56 | */ |
57 | private array $exceptEvents = []; |
58 | |
59 | /** |
60 | * Event listeners of signal. |
61 | * |
62 | * @var array<int, callable> |
63 | */ |
64 | private array $signalEvents = []; |
65 | |
66 | /** |
67 | * Fds waiting for read event. |
68 | * |
69 | * @var array<int, resource> |
70 | */ |
71 | private array $readFds = []; |
72 | |
73 | /** |
74 | * Fds waiting for write event. |
75 | * |
76 | * @var array<int, resource> |
77 | */ |
78 | private array $writeFds = []; |
79 | |
80 | /** |
81 | * Fds waiting for except event. |
82 | * |
83 | * @var array<int, resource> |
84 | */ |
85 | private array $exceptFds = []; |
86 | |
87 | /** |
88 | * Timer scheduler. |
89 | * {['data':timer_id, 'priority':run_timestamp], ..} |
90 | * |
91 | * @var SplPriorityQueue |
92 | */ |
93 | private SplPriorityQueue $scheduler; |
94 | |
95 | /** |
96 | * All timer event listeners. |
97 | * [[func, args, flag, timer_interval], ..] |
98 | * |
99 | * @var array |
100 | */ |
101 | private array $eventTimer = []; |
102 | |
103 | /** |
104 | * Timer id. |
105 | * |
106 | * @var int |
107 | */ |
108 | private int $timerId = 1; |
109 | |
110 | /** |
111 | * Select timeout. |
112 | * |
113 | * @var int |
114 | */ |
115 | private int $selectTimeout = self::MAX_SELECT_TIMOUT_US; |
116 | |
117 | /** |
118 | * Next run time of the timer. |
119 | * |
120 | * @var float |
121 | */ |
122 | private float $nextTickTime = 0; |
123 | |
124 | /** |
125 | * @var ?callable |
126 | */ |
127 | private $errorHandler = null; |
128 | |
129 | /** |
130 | * Select timeout. |
131 | * |
132 | * @var int |
133 | */ |
134 | const MAX_SELECT_TIMOUT_US = 800000; |
135 | |
136 | /** |
137 | * Construct. |
138 | */ |
139 | public function __construct() |
140 | { |
141 | $this->scheduler = new SplPriorityQueue(); |
142 | $this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH); |
143 | } |
144 | |
145 | /** |
146 | * {@inheritdoc} |
147 | */ |
148 | public function delay(float $delay, callable $func, array $args = []): int |
149 | { |
150 | $timerId = $this->timerId++; |
151 | $runTime = microtime(true) + $delay; |
152 | $this->scheduler->insert($timerId, -$runTime); |
153 | $this->eventTimer[$timerId] = [$func, $args]; |
154 | if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) { |
155 | $this->setNextTickTime($runTime); |
156 | } |
157 | return $timerId; |
158 | } |
159 | |
160 | /** |
161 | * {@inheritdoc} |
162 | */ |
163 | public function repeat(float $interval, callable $func, array $args = []): int |
164 | { |
165 | $timerId = $this->timerId++; |
166 | $runTime = microtime(true) + $interval; |
167 | $this->scheduler->insert($timerId, -$runTime); |
168 | $this->eventTimer[$timerId] = [$func, $args, $interval]; |
169 | if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) { |
170 | $this->setNextTickTime($runTime); |
171 | } |
172 | return $timerId; |
173 | } |
174 | |
175 | /** |
176 | * {@inheritdoc} |
177 | */ |
178 | public function offDelay(int $timerId): bool |
179 | { |
180 | if (isset($this->eventTimer[$timerId])) { |
181 | unset($this->eventTimer[$timerId]); |
182 | return true; |
183 | } |
184 | return false; |
185 | } |
186 | |
187 | /** |
188 | * {@inheritdoc} |
189 | */ |
190 | public function offRepeat(int $timerId): bool |
191 | { |
192 | return $this->offDelay($timerId); |
193 | } |
194 | |
195 | /** |
196 | * {@inheritdoc} |
197 | */ |
198 | public function onReadable($stream, callable $func): void |
199 | { |
200 | $count = count($this->readFds); |
201 | if ($count >= 1024) { |
202 | trigger_error("System call select exceeded the maximum number of connections 1024, please install event extension for more connections.", E_USER_WARNING); |
203 | } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) { |
204 | trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING); |
205 | } |
206 | $fdKey = (int)$stream; |
207 | $this->readEvents[$fdKey] = $func; |
208 | $this->readFds[$fdKey] = $stream; |
209 | } |
210 | |
211 | /** |
212 | * {@inheritdoc} |
213 | */ |
214 | public function offReadable($stream): bool |
215 | { |
216 | $fdKey = (int)$stream; |
217 | if (isset($this->readEvents[$fdKey])) { |
218 | unset($this->readEvents[$fdKey], $this->readFds[$fdKey]); |
219 | return true; |
220 | } |
221 | return false; |
222 | } |
223 | |
224 | /** |
225 | * {@inheritdoc} |
226 | */ |
227 | public function onWritable($stream, callable $func): void |
228 | { |
229 | $count = count($this->writeFds); |
230 | if ($count >= 1024) { |
231 | trigger_error("System call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.", E_USER_WARNING); |
232 | } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) { |
233 | trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING); |
234 | } |
235 | $fdKey = (int)$stream; |
236 | $this->writeEvents[$fdKey] = $func; |
237 | $this->writeFds[$fdKey] = $stream; |
238 | } |
239 | |
240 | /** |
241 | * {@inheritdoc} |
242 | */ |
243 | public function offWritable($stream): bool |
244 | { |
245 | $fdKey = (int)$stream; |
246 | if (isset($this->writeEvents[$fdKey])) { |
247 | unset($this->writeEvents[$fdKey], $this->writeFds[$fdKey]); |
248 | return true; |
249 | } |
250 | return false; |
251 | } |
252 | |
253 | /** |
254 | * On except. |
255 | * |
256 | * @param resource $stream |
257 | * @param callable $func |
258 | */ |
259 | public function onExcept($stream, callable $func): void |
260 | { |
261 | $fdKey = (int)$stream; |
262 | $this->exceptEvents[$fdKey] = $func; |
263 | $this->exceptFds[$fdKey] = $stream; |
264 | } |
265 | |
266 | /** |
267 | * Off except. |
268 | * |
269 | * @param resource $stream |
270 | * @return bool |
271 | */ |
272 | public function offExcept($stream): bool |
273 | { |
274 | $fdKey = (int)$stream; |
275 | if (isset($this->exceptEvents[$fdKey])) { |
276 | unset($this->exceptEvents[$fdKey], $this->exceptFds[$fdKey]); |
277 | return true; |
278 | } |
279 | return false; |
280 | } |
281 | |
282 | /** |
283 | * {@inheritdoc} |
284 | */ |
285 | public function onSignal(int $signal, callable $func): void |
286 | { |
287 | if (!function_exists('pcntl_signal')) { |
288 | return; |
289 | } |
290 | $this->signalEvents[$signal] = $func; |
291 | pcntl_signal($signal, fn () => $this->safeCall($this->signalEvents[$signal], [$signal])); |
292 | } |
293 | |
294 | /** |
295 | * {@inheritdoc} |
296 | */ |
297 | public function offSignal(int $signal): bool |
298 | { |
299 | if (!function_exists('pcntl_signal')) { |
300 | return false; |
301 | } |
302 | pcntl_signal($signal, SIG_IGN); |
303 | if (isset($this->signalEvents[$signal])) { |
304 | unset($this->signalEvents[$signal]); |
305 | return true; |
306 | } |
307 | return false; |
308 | } |
309 | |
310 | /** |
311 | * Tick for timer. |
312 | * |
313 | * @return void |
314 | */ |
315 | protected function tick(): void |
316 | { |
317 | $tasksToInsert = []; |
318 | while (!$this->scheduler->isEmpty()) { |
319 | $schedulerData = $this->scheduler->top(); |
320 | $timerId = $schedulerData['data']; |
321 | $nextRunTime = -$schedulerData['priority']; |
322 | $timeNow = microtime(true); |
323 | $this->selectTimeout = (int)(($nextRunTime - $timeNow) * 1000000); |
324 | |
325 | if ($this->selectTimeout <= 0) { |
326 | $this->scheduler->extract(); |
327 | |
328 | if (!isset($this->eventTimer[$timerId])) { |
329 | continue; |
330 | } |
331 | |
332 | // [func, args, timer_interval] |
333 | $taskData = $this->eventTimer[$timerId]; |
334 | if (isset($taskData[2])) { |
335 | $nextRunTime = $timeNow + $taskData[2]; |
336 | $tasksToInsert[] = [$timerId, -$nextRunTime]; |
337 | } else { |
338 | unset($this->eventTimer[$timerId]); |
339 | } |
340 | $this->safeCall($taskData[0], $taskData[1]); |
341 | } else { |
342 | break; |
343 | } |
344 | } |
345 | foreach ($tasksToInsert as $item) { |
346 | $this->scheduler->insert($item[0], $item[1]); |
347 | } |
348 | if (!$this->scheduler->isEmpty()) { |
349 | $schedulerData = $this->scheduler->top(); |
350 | $nextRunTime = -$schedulerData['priority']; |
351 | $this->setNextTickTime($nextRunTime); |
352 | return; |
353 | } |
354 | $this->setNextTickTime(0); |
355 | } |
356 | |
357 | /** |
358 | * Set next tick time. |
359 | * |
360 | * @param float $nextTickTime |
361 | * @return void |
362 | */ |
363 | protected function setNextTickTime(float $nextTickTime): void |
364 | { |
365 | $this->nextTickTime = $nextTickTime; |
366 | if ($nextTickTime == 0) { |
367 | // Swow will affect the signal interruption characteristics of stream_select, |
368 | // so a shorter timeout should be used to detect signals. |
369 | $this->selectTimeout = self::MAX_SELECT_TIMOUT_US; |
370 | return; |
371 | } |
372 | $this->selectTimeout = min(max((int)(($nextTickTime - microtime(true)) * 1000000), 0), self::MAX_SELECT_TIMOUT_US); |
373 | } |
374 | |
375 | /** |
376 | * {@inheritdoc} |
377 | */ |
378 | public function deleteAllTimer(): void |
379 | { |
380 | $this->scheduler = new SplPriorityQueue(); |
381 | $this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH); |
382 | $this->eventTimer = []; |
383 | } |
384 | |
385 | /** |
386 | * {@inheritdoc} |
387 | */ |
388 | public function run(): void |
389 | { |
390 | while ($this->running) { |
391 | $read = $this->readFds; |
392 | $write = $this->writeFds; |
393 | $except = $this->exceptFds; |
394 | if ($read || $write || $except) { |
395 | // Waiting read/write/signal/timeout events. |
396 | try { |
397 | @stream_select($read, $write, $except, 0, $this->selectTimeout); |
398 | } catch (Throwable) { |
399 | // do nothing |
400 | } |
401 | } else { |
402 | $this->selectTimeout >= 1 && usleep($this->selectTimeout); |
403 | } |
404 | |
405 | foreach ($read as $fd) { |
406 | $fdKey = (int)$fd; |
407 | if (isset($this->readEvents[$fdKey])) { |
408 | $this->readEvents[$fdKey]($fd); |
409 | } |
410 | } |
411 | |
412 | foreach ($write as $fd) { |
413 | $fdKey = (int)$fd; |
414 | if (isset($this->writeEvents[$fdKey])) { |
415 | $this->writeEvents[$fdKey]($fd); |
416 | } |
417 | } |
418 | |
419 | foreach ($except as $fd) { |
420 | $fdKey = (int)$fd; |
421 | if (isset($this->exceptEvents[$fdKey])) { |
422 | $this->exceptEvents[$fdKey]($fd); |
423 | } |
424 | } |
425 | |
426 | if ($this->nextTickTime > 0) { |
427 | if (microtime(true) >= $this->nextTickTime) { |
428 | $this->tick(); |
429 | } else { |
430 | $this->selectTimeout = (int)(($this->nextTickTime - microtime(true)) * 1000000); |
431 | } |
432 | } |
433 | |
434 | // The $this->signalEvents are empty under Windows, make sure not to call pcntl_signal_dispatch. |
435 | if ($this->signalEvents) { |
436 | // Calls signal handlers for pending signals |
437 | pcntl_signal_dispatch(); |
438 | } |
439 | } |
440 | } |
441 | |
442 | /** |
443 | * {@inheritdoc} |
444 | */ |
445 | public function stop(): void |
446 | { |
447 | $this->running = false; |
448 | $this->deleteAllTimer(); |
449 | foreach ($this->signalEvents as $signal => $item) { |
450 | $this->offsignal($signal); |
451 | } |
452 | $this->readFds = []; |
453 | $this->writeFds = []; |
454 | $this->exceptFds = []; |
455 | $this->readEvents = []; |
456 | $this->writeEvents = []; |
457 | $this->exceptEvents = []; |
458 | $this->signalEvents = []; |
459 | } |
460 | |
461 | /** |
462 | * {@inheritdoc} |
463 | */ |
464 | public function getTimerCount(): int |
465 | { |
466 | return count($this->eventTimer); |
467 | } |
468 | |
469 | /** |
470 | * {@inheritdoc} |
471 | */ |
472 | public function setErrorHandler(callable $errorHandler): void |
473 | { |
474 | $this->errorHandler = $errorHandler; |
475 | } |
476 | |
477 | /** |
478 | * @param callable $func |
479 | * @param array $args |
480 | * @return void |
481 | */ |
482 | private function safeCall(callable $func, array $args = []): void |
483 | { |
484 | try { |
485 | $func(...$args); |
486 | } catch (Throwable $e) { |
487 | if ($this->errorHandler === null) { |
488 | echo $e; |
489 | } else { |
490 | ($this->errorHandler)($e); |
491 | } |
492 | } |
493 | } |
494 | } |