-
-
Notifications
You must be signed in to change notification settings - Fork 130
[WIP][3.x] Add Io\Poll Event Loop #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 3.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| <?php | ||
|
|
||
| namespace React\EventLoop; | ||
|
|
||
| use React\EventLoop\Tick\FutureTickQueue; | ||
| use React\EventLoop\Timer\Timer; | ||
| use React\EventLoop\Timer\Timers; | ||
| use SplObjectStorage; | ||
|
|
||
| final class IoPollLoop implements LoopInterface | ||
| { | ||
| /** @internal */ | ||
| const MICROSECONDS_PER_SECOND = 1000000; | ||
|
|
||
| private $running = false; | ||
| private $context; | ||
| private $futureTickQueue; | ||
| private $timers; | ||
| private $pcntl = false; | ||
| private $pcntlPoll = false; | ||
| private $signals; | ||
| private $watchers = []; | ||
| private $readListeners = []; | ||
| private $writeListeners = []; | ||
|
|
||
| public function __construct() | ||
| { | ||
| $this->context = new \Io\Poll\Context(); | ||
| $this->futureTickQueue = new FutureTickQueue(); | ||
| $this->timers = new Timers(); | ||
| $this->pcntl = \function_exists('pcntl_signal') && \function_exists('pcntl_signal_dispatch'); | ||
| $this->pcntlPoll = $this->pcntl && !\function_exists('pcntl_async_signals'); | ||
| $this->signals = new SignalsHandler(); | ||
|
|
||
| // prefer async signals if available (PHP 7.1+) or fall back to dispatching on each tick | ||
| if ($this->pcntl && !$this->pcntlPoll) { | ||
| \pcntl_async_signals(true); | ||
| } | ||
| } | ||
|
|
||
| public function addReadStream($stream, $listener) | ||
| { | ||
| $key = (int) $stream; | ||
| if (!isset($this->readListeners[$key])) { | ||
| $this->readListeners[$key] = $listener; | ||
| } | ||
| $this->manageStream($key, $stream, \Io\Poll\Event::Read, true); | ||
| } | ||
|
|
||
| public function addWriteStream($stream, $listener) | ||
| { | ||
| $key = (int) $stream; | ||
| if (!isset($this->writeListeners[$key])) { | ||
| $this->writeListeners[$key] = $listener; | ||
| } | ||
| $this->manageStream($key, $stream, \Io\Poll\Event::Write, true); | ||
| } | ||
|
|
||
| public function removeReadStream($stream) | ||
| { | ||
| $key = (int) $stream; | ||
| unset($this->readListeners[$key]); | ||
| $this->manageStream($key, $stream, \Io\Poll\Event::Read, false); | ||
| } | ||
|
|
||
| public function removeWriteStream($stream) | ||
| { | ||
| $key = (int) $stream; | ||
| unset($this->writeListeners[$key]); | ||
| $this->manageStream($key, $stream, \Io\Poll\Event::Write, false); | ||
| } | ||
|
|
||
| public function addTimer($interval, $callback) | ||
| { | ||
| $timer = new Timer($interval, $callback, false); | ||
|
|
||
| $this->timers->add($timer); | ||
|
|
||
| return $timer; | ||
| } | ||
|
|
||
| public function addPeriodicTimer($interval, $callback) | ||
| { | ||
| $timer = new Timer($interval, $callback, true); | ||
|
|
||
| $this->timers->add($timer); | ||
|
|
||
| return $timer; | ||
| } | ||
|
|
||
| public function cancelTimer(TimerInterface $timer) | ||
| { | ||
| $this->timers->cancel($timer); | ||
| } | ||
|
|
||
| public function futureTick($listener) | ||
| { | ||
| $this->futureTickQueue->add($listener); | ||
| } | ||
|
|
||
| public function addSignal($signal, $listener) | ||
| { | ||
| if ($this->pcntl === false) { | ||
| throw new \BadMethodCallException('Event loop feature "signals" isn\'t supported by the "StreamSelectLoop"'); | ||
| } | ||
|
|
||
| $first = $this->signals->count($signal) === 0; | ||
| $this->signals->add($signal, $listener); | ||
|
|
||
| if ($first) { | ||
| \pcntl_signal($signal, [$this->signals, 'call']); | ||
| } | ||
| } | ||
|
|
||
| public function removeSignal($signal, $listener) | ||
| { | ||
| if (!$this->signals->count($signal)) { | ||
| return; | ||
| } | ||
|
|
||
| $this->signals->remove($signal, $listener); | ||
|
|
||
| if ($this->signals->count($signal) === 0) { | ||
| \pcntl_signal($signal, \SIG_DFL); | ||
| } | ||
| } | ||
|
|
||
| public function run() | ||
| { | ||
| $this->running = true; | ||
|
|
||
| while ($this->running) { | ||
| $this->futureTickQueue->tick(); | ||
|
|
||
| $this->timers->tick(); | ||
|
|
||
| // Future-tick queue has pending callbacks ... | ||
| if (!$this->futureTickQueue->isEmpty()) { | ||
| $timeout = 0; | ||
|
|
||
| // There is a pending timer, only block until it is due ... | ||
| } elseif ($scheduledAt = $this->timers->getFirst()) { | ||
| $timeout = $scheduledAt - $this->timers->getTime(); | ||
| if ($timeout < 0) { | ||
| $timeout = 0; | ||
| } else { | ||
| // Convert float seconds to int microseconds. | ||
| // Ensure we do not exceed maximum integer size, which may | ||
| // cause the loop to tick once every ~35min on 32bit systems. | ||
| $timeout *= self::MICROSECONDS_PER_SECOND; | ||
| $timeout = $timeout > \PHP_INT_MAX ? \PHP_INT_MAX : (int)$timeout; | ||
| } | ||
|
|
||
| // The only possible event is stream or signal activity, so wait forever ... | ||
| } elseif ($this->readListeners || $this->writeListeners || !$this->signals->isEmpty()) { | ||
| $timeout = null; | ||
|
|
||
| // There's nothing left to do ... | ||
| } else { | ||
| break; | ||
| } | ||
|
|
||
| foreach ($this->context->wait(\Time\Duration::fromMicroseconds((float) $timeout)) as $watcher) { | ||
| $stream = $watcher->getHandle()->getStream(); | ||
| $key = (int) $stream; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can probably use the associated
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hoping to, noticed it when getting the first working run. This is pretty much a copy of the P.S. Thanks for the early feedback <3 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Appreciated. I really wanted to get Time\Duration into PHP 8.6 last minute so that the polling API doesn't start right of with a “meh” API. |
||
|
|
||
| if (in_array(\Io\Poll\Event::Read, $watcher->getTriggeredEvents()) && array_key_exists($key, $this->readListeners)) { | ||
| \call_user_func($this->readListeners[$key], $stream); | ||
| } | ||
|
|
||
| if (in_array(\Io\Poll\Event::Write, $watcher->getTriggeredEvents()) && array_key_exists($key, $this->writeListeners)) { | ||
| \call_user_func($this->writeListeners[$key], $stream); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public function stop() | ||
| { | ||
| $this->running = false; | ||
| } | ||
|
|
||
| private function manageStream($key, $stream, \Io\Poll\Event $event, bool $add) | ||
| { | ||
| if (!array_key_exists($key, $this->watchers)) { | ||
| if (!$add) { | ||
| return; | ||
| } | ||
|
|
||
| $handle = new \StreamPollHandle($stream); | ||
| $this->watchers[$key] = $this->context->add($handle, [$event]); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $events = $this->watchers[$key]->getWatchedEvents(); | ||
| $events = array_filter($events, function (\Io\Poll\Event $e) use ($event) { | ||
| return $e === $event; | ||
| }); | ||
| if ($add) { | ||
| $events[] = $event; | ||
| } | ||
| $this->watchers[$key]->modifyEvents($events); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?php | ||
|
|
||
| namespace React\Tests\EventLoop; | ||
|
|
||
| use React\EventLoop\IoPollLoop; | ||
|
|
||
| class IoPollLoopTest extends \React\Tests\EventLoop\AbstractLoopTest | ||
| { | ||
| public function createLoop() | ||
| { | ||
| if (\class_exists('Io\Poll\Context', false)) { | ||
| $this->markTestSkipped('IOPollLoop tests skipped because IO Poll is not available.'); | ||
| } | ||
|
|
||
| return new IoPollLoop(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fromMicroseconds()doesn't take a float. This cast looks fishy (particularly since you will also cast$timout = null.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was running beta3 locally, I guess that changed in RC1? As it threw errors at me when passing it an int.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the implementation is effectively unchanged since it initially landed: https://github.com/php/php-src/commits/master/ext/date/time_duration.c.
intis definitely what is expected: https://github.com/php/php-src/blob/3d979856590e19ac533e29c21d634c991ac6e9f8/ext/date/time.stub.php#L36-L38 / https://github.com/php/php-src/blob/3d979856590e19ac533e29c21d634c991ac6e9f8/ext/date/time_duration.c#L247Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And generally speaking, you likely want the
Duration::fromSeconds()constructor here. Something like:should hopefully work.