diff --git a/.travis.yml b/.travis.yml index 37ba1989..0340c258 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ before_script: cd libevent-0.0.5 && phpize && ./configure && make && sudo make install; echo "extension=libevent.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`; fi" + - echo "yes" | pecl install event - composer self-update - composer install --dev --prefer-source diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 33fa124b..1c6298a1 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -2,9 +2,13 @@ namespace React\EventLoop; -use SplObjectStorage; +use libev\EventLoop; +use libev\IOEvent; +use libev\TimerEvent; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; +use SplObjectStorage; /** * @see https://github.com/m4rw3r/php-libev @@ -13,140 +17,184 @@ class LibEvLoop implements LoopInterface { private $loop; - private $timers; - private $readEvents = array(); - private $writeEvents = array(); + private $nextTickQueue; + private $timerEvents; + private $readEvents = []; + private $writeEvents = []; + private $running; public function __construct() { - $this->loop = new \libev\EventLoop(); - $this->timers = new SplObjectStorage(); + $this->loop = new EventLoop(); + $this->nextTickQueue = new NextTickQueue($this); + $this->timerEvents = new SplObjectStorage(); } - public function addReadStream($stream, $listener) + /** + * {@inheritdoc} + */ + public function addReadStream($stream, callable $listener) { - $this->addStream($stream, $listener, \libev\IOEvent::READ); + $callback = function () use ($stream, $listener) { + call_user_func($listener, $stream, $this); + }; + + $event = new IOEvent($callback, $stream, IOEvent::READ); + $this->loop->add($event); + + $this->readEvents[(int) $stream] = $event; } - public function addWriteStream($stream, $listener) + /** + * {@inheritdoc} + */ + public function addWriteStream($stream, callable $listener) { - $this->addStream($stream, $listener, \libev\IOEvent::WRITE); + $callback = function () use ($stream, $listener) { + call_user_func($listener, $stream, $this); + }; + + $event = new IOEvent($callback, $stream, IOEvent::WRITE); + $this->loop->add($event); + + $this->writeEvents[(int) $stream] = $event; } + /** + * {@inheritdoc} + */ public function removeReadStream($stream) { - if (isset($this->readEvents[(int)$stream])) { - $this->readEvents[(int)$stream]->stop(); - unset($this->readEvents[(int)$stream]); + $key = (int) $stream; + + if (isset($this->readEvents[$key])) { + $this->readEvents[$key]->stop(); + unset($this->readEvents[$key]); } } + /** + * {@inheritdoc} + */ public function removeWriteStream($stream) { - if (isset($this->writeEvents[(int)$stream])) { - $this->writeEvents[(int)$stream]->stop(); - unset($this->writeEvents[(int)$stream]); + $key = (int) $stream; + + if (isset($this->writeEvents[$key])) { + $this->writeEvents[$key]->stop(); + unset($this->writeEvents[$key]); } } + /** + * {@inheritdoc} + */ public function removeStream($stream) { $this->removeReadStream($stream); $this->removeWriteStream($stream); } - private function addStream($stream, $listener, $flags) + /** + * {@inheritdoc} + */ + public function addTimer($interval, callable $callback) { - $listener = $this->wrapStreamListener($stream, $listener, $flags); - $event = new \libev\IOEvent($listener, $stream, $flags); - $this->loop->add($event); - - if (($flags & \libev\IOEvent::READ) === $flags) { - $this->readEvents[(int)$stream] = $event; - } elseif (($flags & \libev\IOEvent::WRITE) === $flags) { - $this->writeEvents[(int)$stream] = $event; - } - } + $timer = new Timer($this, $interval, $callback, false); - private function wrapStreamListener($stream, $listener, $flags) - { - if (($flags & \libev\IOEvent::READ) === $flags) { - $removeCallback = array($this, 'removeReadStream'); - } elseif (($flags & \libev\IOEvent::WRITE) === $flags) { - $removeCallback = array($this, 'removeWriteStream'); - } + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); - return function ($event) use ($stream, $listener, $removeCallback) { - call_user_func($listener, $stream); + if ($this->isTimerActive($timer)) { + $this->cancelTimer($timer); + } }; - } - public function addTimer($interval, $callback) - { - $timer = new Timer($this, $interval, $callback, false); - $this->setupTimer($timer); + $event = new TimerEvent($callback, $timer->getInterval()); + $this->timerEvents->attach($timer, $event); + $this->loop->add($event); return $timer; } - public function addPeriodicTimer($interval, $callback) + /** + * {@inheritdoc} + */ + public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); - $this->setupTimer($timer); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + }; + + $event = new TimerEvent($callback, $interval, $interval); + $this->timerEvents->attach($timer, $event); + $this->loop->add($event); return $timer; } + /** + * {@inheritdoc} + */ public function cancelTimer(TimerInterface $timer) { - if (isset($this->timers[$timer])) { - $this->loop->remove($this->timers[$timer]); - $this->timers->detach($timer); + if (isset($this->timerEvents[$timer])) { + $this->loop->remove($this->timerEvents[$timer]); + $this->timerEvents->detach($timer); } } - private function setupTimer(TimerInterface $timer) + /** + * {@inheritdoc} + */ + public function isTimerActive(TimerInterface $timer) { - $dummyCallback = function () {}; - $interval = $timer->getInterval(); - - if ($timer->isPeriodic()) { - $libevTimer = new \libev\TimerEvent($dummyCallback, $interval, $interval); - } else { - $libevTimer = new \libev\TimerEvent($dummyCallback, $interval); - } - - $libevTimer->setCallback(function () use ($timer) { - call_user_func($timer->getCallback(), $timer); - - if (!$timer->isPeriodic()) { - $timer->cancel(); - } - }); - - $this->timers->attach($timer, $libevTimer); - $this->loop->add($libevTimer); - - return $timer; + return $this->timerEvents->contains($timer); } - public function isTimerActive(TimerInterface $timer) + /** + * {@inheritdoc} + */ + public function nextTick(callable $listener) { - return $this->timers->contains($timer); + $this->nextTickQueue->add($listener); } + /** + * {@inheritdoc} + */ public function tick() { - $this->loop->run(\libev\EventLoop::RUN_ONCE); + $this->nextTickQueue->tick(); + + $this->loop->run(EventLoop::RUN_ONCE | EventLoop::RUN_NOWAIT); } + /** + * {@inheritdoc} + */ public function run() { - $this->loop->run(); + $this->running = true; + + while ($this->running) { + $this->nextTickQueue->tick(); + + if (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count()) { + break; + } + + $this->loop->run(EventLoop::RUN_ONCE); + } } + /** + * {@inheritdoc} + */ public function stop() { - $this->loop->breakLoop(); + $this->running = false; } } diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 7fb27d6d..599cb63b 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -2,221 +2,322 @@ namespace React\EventLoop; -use SplObjectStorage; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; +use SplObjectStorage; +/** + * An ext-libevent based event-loop. + */ class LibEventLoop implements LoopInterface { - const MIN_TIMER_RESOLUTION = 0.001; - - private $base; - private $callback; - private $timers; - - private $events = array(); - private $flags = array(); - private $readCallbacks = array(); - private $writeCallbacks = array(); + const MICROSECONDS_PER_SECOND = 1000000; + + private $eventBase; + private $nextTickQueue; + private $timerCallback; + private $timerEvents; + private $streamCallback; + private $streamEvents = []; + private $streamFlags = []; + private $readListeners = []; + private $writeListeners = []; + private $running; public function __construct() { - $this->base = event_base_new(); - $this->callback = $this->createLibeventCallback(); - $this->timers = new SplObjectStorage(); + $this->eventBase = event_base_new(); + $this->nextTickQueue = new NextTickQueue($this); + $this->timerEvents = new SplObjectStorage(); + + $this->createTimerCallback(); + $this->createStreamCallback(); } - protected function createLibeventCallback() + /** + * {@inheritdoc} + */ + public function addReadStream($stream, callable $listener) { - $readCallbacks = &$this->readCallbacks; - $writeCallbacks = &$this->writeCallbacks; - - return function ($stream, $flags, $loop) use (&$readCallbacks, &$writeCallbacks) { - $id = (int) $stream; - - try { - if (($flags & EV_READ) === EV_READ && isset($readCallbacks[$id])) { - call_user_func($readCallbacks[$id], $stream, $loop); - } + $key = (int) $stream; - if (($flags & EV_WRITE) === EV_WRITE && isset($writeCallbacks[$id])) { - call_user_func($writeCallbacks[$id], $stream, $loop); - } - } catch (\Exception $ex) { - // If one of the callbacks throws an exception we must stop the loop - // otherwise libevent will swallow the exception and go berserk. - $loop->stop(); - - throw $ex; - } - }; + if (!isset($this->readListeners[$key])) { + $this->readListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, EV_READ); + } } - public function addReadStream($stream, $listener) + /** + * {@inheritdoc} + */ + public function addWriteStream($stream, callable $listener) { - $this->addStreamEvent($stream, EV_READ, 'read', $listener); + $key = (int) $stream; + + if (!isset($this->writeListeners[$key])) { + $this->writeListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, EV_WRITE); + } } - public function addWriteStream($stream, $listener) + /** + * {@inheritdoc} + */ + public function removeReadStream($stream) { - $this->addStreamEvent($stream, EV_WRITE, 'write', $listener); + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + unset($this->readListeners[$key]); + $this->unsubscribeStreamEvent($stream, EV_READ); + } } - protected function addStreamEvent($stream, $eventClass, $type, $listener) + /** + * {@inheritdoc} + */ + public function removeWriteStream($stream) { - $id = (int) $stream; + $key = (int) $stream; - if ($existing = isset($this->events[$id])) { - if (($this->flags[$id] & $eventClass) === $eventClass) { - return; - } - $event = $this->events[$id]; - event_del($event); - } else { - $event = event_new(); + if (isset($this->writeListeners[$key])) { + unset($this->writeListeners[$key]); + $this->unsubscribeStreamEvent($stream, EV_WRITE); } + } - $flags = isset($this->flags[$id]) ? $this->flags[$id] | $eventClass : $eventClass; - event_set($event, $stream, $flags | EV_PERSIST, $this->callback, $this); + /** + * {@inheritdoc} + */ + public function removeStream($stream) + { + $key = (int) $stream; - if (!$existing) { - // Set the base only if $event has been newly created or be ready for segfaults. - event_base_set($event, $this->base); - } + if (isset($this->streamEvents[$key])) { + $event = $this->streamEvents[$key]; - event_add($event); + event_del($event); + event_free($event); - $this->events[$id] = $event; - $this->flags[$id] = $flags; - $this->{"{$type}Callbacks"}[$id] = $listener; + unset( + $this->streamFlags[$key], + $this->streamEvents[$key], + $this->readListeners[$key], + $this->writeListeners[$key] + ); + } } - public function removeReadStream($stream) + /** + * {@inheritdoc} + */ + public function addTimer($interval, callable $callback) { - $this->removeStreamEvent($stream, EV_READ, 'read'); - } + $timer = new Timer($this, $interval, $callback, false); - public function removeWriteStream($stream) - { - $this->removeStreamEvent($stream, EV_WRITE, 'write'); + $this->scheduleTimer($timer); + + return $timer; } - protected function removeStreamEvent($stream, $eventClass, $type) + /** + * {@inheritdoc} + */ + public function addPeriodicTimer($interval, callable $callback) { - $id = (int) $stream; + $timer = new Timer($this, $interval, $callback, true); - if (isset($this->events[$id])) { - $flags = $this->flags[$id] & ~$eventClass; + $this->scheduleTimer($timer); - if ($flags === 0) { - // Remove if stream is not subscribed to any event at this point. - return $this->removeStream($stream); - } + return $timer; + } - $event = $this->events[$id]; + /** + * {@inheritdoc} + */ + public function cancelTimer(TimerInterface $timer) + { + if ($this->isTimerActive($timer)) { + $event = $this->timerEvents[$timer]; event_del($event); event_free($event); - unset($this->{"{$type}Callbacks"}[$id]); - - $event = event_new(); - event_set($event, $stream, $flags | EV_PERSIST, $this->callback, $this); - event_base_set($event, $this->base); - event_add($event); - $this->events[$id] = $event; - $this->flags[$id] = $flags; + $this->timerEvents->detach($timer); } } - public function removeStream($stream) + /** + * {@inheritdoc} + */ + public function isTimerActive(TimerInterface $timer) { - $id = (int) $stream; - - if (isset($this->events[$id])) { - $event = $this->events[$id]; - - unset( - $this->events[$id], - $this->flags[$id], - $this->readCallbacks[$id], - $this->writeCallbacks[$id] - ); + return $this->timerEvents->contains($timer); + } - event_del($event); - event_free($event); - } + /** + * {@inheritdoc} + */ + public function nextTick(callable $listener) + { + $this->nextTickQueue->add($listener); } - protected function addTimerInternal($interval, $callback, $periodic = false) + /** + * {@inheritdoc} + */ + public function tick() { - if ($interval < self::MIN_TIMER_RESOLUTION) { - throw new \InvalidArgumentException('Timer events do not support sub-millisecond timeouts.'); - } + $this->nextTickQueue->tick(); - $timer = new Timer($this, $interval, $callback, $periodic); - $resource = event_new(); + event_base_loop($this->eventBase, EVLOOP_ONCE | EVLOOP_NONBLOCK); + } - $timers = $this->timers; - $timers->attach($timer, $resource); + /** + * {@inheritdoc} + */ + public function run() + { + $this->running = true; - $callback = function () use ($timers, $timer, &$callback) { - if (isset($timers[$timer])) { - call_user_func($timer->getCallback(), $timer); + while ($this->running) { + $this->nextTickQueue->tick(); - if ($timer->isPeriodic() && isset($timers[$timer])) { - event_add($timers[$timer], $timer->getInterval() * 1000000); - } else { - $timer->cancel(); - } + if (!$this->streamEvents && !$this->timerEvents->count()) { + break; } - }; - - event_timer_set($resource, $callback); - event_base_set($resource, $this->base); - event_add($resource, $interval * 1000000); - return $timer; + event_base_loop($this->eventBase, EVLOOP_ONCE); + } } - public function addTimer($interval, $callback) + /** + * {@inheritdoc} + */ + public function stop() { - return $this->addTimerInternal($interval, $callback); + $this->running = false; } - public function addPeriodicTimer($interval, $callback) + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + private function scheduleTimer(TimerInterface $timer) { - return $this->addTimerInternal($interval, $callback, true); + $this->timerEvents[$timer] = $event = event_timer_new(); + + event_timer_set($event, $this->timerCallback, $timer); + event_base_set($event, $this->eventBase); + event_add($event, $timer->getInterval() * self::MICROSECONDS_PER_SECOND); } - public function cancelTimer(TimerInterface $timer) + /** + * Create a new ext-libevent event resource, or update the existing one. + * + * @param stream $stream + * @param integer $flag EV_READ or EV_WRITE + */ + private function subscribeStreamEvent($stream, $flag) { - if (isset($this->timers[$timer])) { - $resource = $this->timers[$timer]; - event_del($resource); - event_free($resource); + $key = (int) $stream; + + if (isset($this->streamEvents[$key])) { + $event = $this->streamEvents[$key]; + $flags = $this->streamFlags[$key] |= $flag; - $this->timers->detach($timer); + event_del($event); + event_set($event, $stream, EV_PERSIST | $flags, $this->streamCallback); + } else { + $event = event_new(); + + event_set($event, $stream, EV_PERSIST | $flag, $this->streamCallback); + event_base_set($event, $this->eventBase); + + $this->streamEvents[$key] = $event; + $this->streamFlags[$key] = $flag; } - } - public function isTimerActive(TimerInterface $timer) - { - return $this->timers->contains($timer); + event_add($event); } - public function tick() + /** + * Update the ext-libevent event resource for this stream to stop listening to + * the given event type, or remove it entirely if it's no longer needed. + * + * @param stream $stream + * @param integer $flag EV_READ or EV_WRITE + */ + private function unsubscribeStreamEvent($stream, $flag) { - event_base_loop($this->base, EVLOOP_ONCE | EVLOOP_NONBLOCK); + $key = (int) $stream; + + $flags = $this->streamFlags[$key] &= ~$flag; + + if (0 === $flags) { + $this->removeStream($stream); + + return; + } + + $event = $this->streamEvents[$key]; + + event_del($event); + event_set($event, $stream, EV_PERSIST | $flags, $this->streamCallback); + event_add($event); } - public function run() + /** + * Create a callback used as the target of timer events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createTimerCallback() { - event_base_loop($this->base); + $this->timerCallback = function ($_, $_, $timer) { + call_user_func($timer->getCallback(), $timer); + + // Timer already cancelled ... + if (!$this->isTimerActive($timer)) { + return; + + // Reschedule periodic timers ... + } elseif ($timer->isPeriodic()) { + event_add( + $this->timerEvents[$timer], + $timer->getInterval() * self::MICROSECONDS_PER_SECOND + ); + + // Clean-up one shot timers ... + } else { + $this->cancelTimer($timer); + } + }; } - public function stop() + /** + * Create a callback used as the target of stream events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createStreamCallback() { - event_base_loopexit($this->base); + $this->streamCallback = function ($stream, $flags) { + $key = (int) $stream; + + if (EV_READ === (EV_READ & $flags) && isset($this->readListeners[$key])) { + call_user_func($this->readListeners[$key], $stream, $this); + } + + if (EV_WRITE === (EV_WRITE & $flags) && isset($this->writeListeners[$key])) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + }; } } diff --git a/src/React/EventLoop/LoopInterface.php b/src/React/EventLoop/LoopInterface.php index 37015523..fc1650fc 100644 --- a/src/React/EventLoop/LoopInterface.php +++ b/src/React/EventLoop/LoopInterface.php @@ -6,19 +6,107 @@ interface LoopInterface { - public function addReadStream($stream, $listener); - public function addWriteStream($stream, $listener); + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addReadStream($stream, callable $listener); + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addWriteStream($stream, callable $listener); + + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeReadStream($stream); + + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeWriteStream($stream); + + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeStream($stream); - public function addTimer($interval, $callback); - public function addPeriodicTimer($interval, $callback); + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, callable $callback); + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, callable $callback); + + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ public function cancelTimer(TimerInterface $timer); + + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ public function isTimerActive(TimerInterface $timer); + /** + * Schedule a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listener The callback to invoke. + */ + public function nextTick(callable $listener); + + /** + * Perform a single iteration of the event loop. + */ public function tick(); + + /** + * Run the event loop until there are no more tasks to perform. + */ public function run(); + + /** + * Instruct a running event loop to stop. + */ public function stop(); } diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index 92cc6dd2..a309eea3 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -2,188 +2,241 @@ namespace React\EventLoop; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use React\EventLoop\Timer\Timers; +/** + * A stream_select() based event-loop. + */ class StreamSelectLoop implements LoopInterface { - const QUANTUM_INTERVAL = 1000000; - + private $nextTickQueue; private $timers; - private $running = false; - private $readStreams = array(); - private $readListeners = array(); - private $writeStreams = array(); - private $writeListeners = array(); + private $readStreams = []; + private $readListeners = []; + private $writeStreams = []; + private $writeListeners = []; + private $running; public function __construct() { + $this->nextTickQueue = new NextTickQueue($this); $this->timers = new Timers(); } - public function addReadStream($stream, $listener) + /** + * {@inheritdoc} + */ + public function addReadStream($stream, callable $listener) { - $id = (int) $stream; + $key = (int) $stream; - if (!isset($this->readStreams[$id])) { - $this->readStreams[$id] = $stream; - $this->readListeners[$id] = $listener; + if (!isset($this->readStreams[$key])) { + $this->readStreams[$key] = $stream; + $this->readListeners[$key] = $listener; } } - public function addWriteStream($stream, $listener) + /** + * {@inheritdoc} + */ + public function addWriteStream($stream, callable $listener) { - $id = (int) $stream; + $key = (int) $stream; - if (!isset($this->writeStreams[$id])) { - $this->writeStreams[$id] = $stream; - $this->writeListeners[$id] = $listener; + if (!isset($this->writeStreams[$key])) { + $this->writeStreams[$key] = $stream; + $this->writeListeners[$key] = $listener; } } + /** + * {@inheritdoc} + */ public function removeReadStream($stream) { - $id = (int) $stream; + $key = (int) $stream; unset( - $this->readStreams[$id], - $this->readListeners[$id] + $this->readStreams[$key], + $this->readListeners[$key] ); } + /** + * {@inheritdoc} + */ public function removeWriteStream($stream) { - $id = (int) $stream; + $key = (int) $stream; unset( - $this->writeStreams[$id], - $this->writeListeners[$id] + $this->writeStreams[$key], + $this->writeListeners[$key] ); } + /** + * {@inheritdoc} + */ public function removeStream($stream) { $this->removeReadStream($stream); $this->removeWriteStream($stream); } - public function addTimer($interval, $callback) + /** + * {@inheritdoc} + */ + public function addTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, false); + $this->timers->add($timer); return $timer; } - public function addPeriodicTimer($interval, $callback) + /** + * {@inheritdoc} + */ + public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); + $this->timers->add($timer); return $timer; } + /** + * {@inheritdoc} + */ public function cancelTimer(TimerInterface $timer) { $this->timers->cancel($timer); } + /** + * {@inheritdoc} + */ public function isTimerActive(TimerInterface $timer) { return $this->timers->contains($timer); } - protected function getNextEventTimeInMicroSeconds() + /** + * {@inheritdoc} + */ + public function nextTick(callable $listener) { - $nextEvent = $this->timers->getFirst(); - - if (null === $nextEvent) { - return self::QUANTUM_INTERVAL; - } - - $currentTime = microtime(true); - if ($nextEvent > $currentTime) { - return ($nextEvent - $currentTime) * 1000000; - } - - return 0; + $this->nextTickQueue->add($listener); } - protected function sleepOnPendingTimers() + /** + * {@inheritdoc} + */ + public function tick() { - if ($this->timers->isEmpty()) { - $this->running = false; - } else { - // We use usleep() instead of stream_select() to emulate timeouts - // since the latter fails when there are no streams registered for - // read / write events. Blame PHP for us needing this hack. - usleep($this->getNextEventTimeInMicroSeconds()); - } + $this->nextTickQueue->tick(); + + $this->timers->tick(); + + $this->waitForStreamActivity(0); } - protected function runStreamSelect($block) + /** + * {@inheritdoc} + */ + public function run() { - $read = $this->readStreams ?: null; - $write = $this->writeStreams ?: null; - $except = null; + $this->running = true; - if (!$read && !$write) { - if ($block) { - $this->sleepOnPendingTimers(); - } + while ($this->running) { + $this->nextTickQueue->tick(); - return; - } + $this->timers->tick(); - $timeout = $block ? $this->getNextEventTimeInMicroSeconds() : 0; + // Timers have placed more items on the next-tick queue ... + if (!$this->nextTickQueue->isEmpty()) { + $timeout = 0; - if (stream_select($read, $write, $except, 0, $timeout) > 0) { - if ($read) { - foreach ($read as $stream) { - if (!isset($this->readListeners[(int) $stream])) { - continue; - } - - $listener = $this->readListeners[(int) $stream]; - call_user_func($listener, $stream, $this); + // There is a pending timer, only block until it is due ... + } elseif ($scheduledAt = $this->timers->getFirst()) { + if (0 > $timeout = $scheduledAt - $this->timers->getTime()) { + $timeout = 0; } - } - if ($write) { - foreach ($write as $stream) { - if (!isset($this->writeListeners[(int) $stream])) { - continue; - } + // The only possible event is stream activity, so wait forever ... + } elseif ($this->readStreams || $this->writeStreams) { + $timeout = null; - $listener = $this->writeListeners[(int) $stream]; - call_user_func($listener, $stream, $this); - } + // There's nothing left to do ... + } else { + break; } + + $this->waitForStreamActivity($timeout); } } - protected function loop($block = true) + /** + * {@inheritdoc} + */ + public function stop() { - $this->timers->tick(); - $this->runStreamSelect($block); - - return $this->running; + $this->running = false; } - public function tick() + /** + * Wait/check for stream activity, or until the next timer is due. + */ + private function waitForStreamActivity($timeout) { - return $this->loop(false); - } + $read = $this->readStreams; + $write = $this->writeStreams; - public function run() - { - $this->running = true; - while ($this->loop()); + $this->streamSelect($read, $write, $timeout); + + foreach ($read as $stream) { + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + call_user_func($this->readListeners[$key], $stream, $this); + } + } + + foreach ($write as $stream) { + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + } } - public function stop() + /** + * Emulate a stream_select() implementation that does not break when passed + * empty stream arrays. + * + * @param array &$read An array of read streams to select upon. + * @param array &$write An array of write streams to select upon. + * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + * + * @return integer The total number of streams that are ready for read/write. + */ + protected function streamSelect(array &$read, array &$write, $timeout) { - $this->running = false; + if ($read || $write) { + $except = null; + + return stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + } + + usleep($timeout); + + return 0; } } diff --git a/src/React/EventLoop/Tick/NextTickQueue.php b/src/React/EventLoop/Tick/NextTickQueue.php new file mode 100644 index 00000000..5b8e1de8 --- /dev/null +++ b/src/React/EventLoop/Tick/NextTickQueue.php @@ -0,0 +1,57 @@ +eventLoop = $eventLoop; + $this->queue = new SplQueue(); + } + + /** + * Add a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listener The callback to invoke. + */ + public function add(callable $listener) + { + $this->queue->enqueue($listener); + } + + /** + * Flush the callback queue. + */ + public function tick() + { + while (!$this->queue->isEmpty()) { + call_user_func( + $this->queue->dequeue(), + $this->eventLoop + ); + } + } + + /** + * Check if the next tick queue is empty. + * + * @return boolean + */ + public function isEmpty() + { + return $this->queue->isEmpty(); + } +} diff --git a/src/React/EventLoop/Timer/Timer.php b/src/React/EventLoop/Timer/Timer.php index 9fc72f47..ac64d2b0 100644 --- a/src/React/EventLoop/Timer/Timer.php +++ b/src/React/EventLoop/Timer/Timer.php @@ -2,21 +2,22 @@ namespace React\EventLoop\Timer; -use InvalidArgumentException; use React\EventLoop\LoopInterface; class Timer implements TimerInterface { + const MIN_INTERVAL = 0.000001; + protected $loop; protected $interval; protected $callback; protected $periodic; protected $data; - public function __construct(LoopInterface $loop, $interval, $callback, $periodic = false, $data = null) + public function __construct(LoopInterface $loop, $interval, callable $callback, $periodic = false, $data = null) { - if (false === is_callable($callback)) { - throw new InvalidArgumentException('The callback argument must be a valid callable object'); + if ($interval < self::MIN_INTERVAL) { + $interval = self::MIN_INTERVAL; } $this->loop = $loop; diff --git a/src/React/EventLoop/Timer/Timers.php b/src/React/EventLoop/Timer/Timers.php index 520158c4..c183a637 100644 --- a/src/React/EventLoop/Timer/Timers.php +++ b/src/React/EventLoop/Timer/Timers.php @@ -4,12 +4,9 @@ use SplObjectStorage; use SplPriorityQueue; -use InvalidArgumentException; class Timers { - const MIN_RESOLUTION = 0.001; - private $time; private $timers; private $scheduler; @@ -33,11 +30,6 @@ public function getTime() public function add(TimerInterface $timer) { $interval = $timer->getInterval(); - - if ($interval < self::MIN_RESOLUTION) { - throw new InvalidArgumentException('Timer events do not support sub-millisecond timeouts.'); - } - $scheduledAt = $interval + $this->getTime(); $this->timers->attach($timer, $scheduledAt); @@ -56,13 +48,17 @@ public function cancel(TimerInterface $timer) public function getFirst() { - if ($this->scheduler->isEmpty()) { - return null; - } + while ($this->scheduler->count()) { + $timer = $this->scheduler->top(); + + if ($this->timers->contains($timer)) { + return $this->timers[$timer]; + } - $scheduledAt = $this->timers[$this->scheduler->top()]; + $this->scheduler->extract(); + } - return $scheduledAt; + return null; } public function isEmpty() diff --git a/tests/React/Tests/Dns/Query/RetryExecutorTest.php b/tests/React/Tests/Dns/Query/RetryExecutorTest.php index 4bf7e23f..fd3d02e6 100644 --- a/tests/React/Tests/Dns/Query/RetryExecutorTest.php +++ b/tests/React/Tests/Dns/Query/RetryExecutorTest.php @@ -172,7 +172,7 @@ protected function createPromiseMock() protected function createStandardResponse() { - $response = new Message; + $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); diff --git a/tests/React/Tests/EventLoop/AbstractLoopTest.php b/tests/React/Tests/EventLoop/AbstractLoopTest.php index ec2d51d1..40b237ba 100644 --- a/tests/React/Tests/EventLoop/AbstractLoopTest.php +++ b/tests/React/Tests/EventLoop/AbstractLoopTest.php @@ -15,24 +15,33 @@ public function setUp() abstract public function createLoop(); + public function createStream() + { + return fopen('php://temp', 'r+'); + } + + public function writeToStream($stream, $content) + { + fwrite($stream, $content); + rewind($stream); + } + public function testAddReadStream() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableExactly(2)); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->loop->tick(); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } public function testAddWriteStream() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addWriteStream($input, $this->expectCallableExactly(2)); $this->loop->tick(); @@ -41,36 +50,33 @@ public function testAddWriteStream() public function testRemoveReadStreamInstantly() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableNever()); $this->loop->removeReadStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } public function testRemoveReadStreamAfterReading() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableOnce()); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->loop->tick(); $this->loop->removeReadStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } public function testRemoveWriteStreamInstantly() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addWriteStream($input, $this->expectCallableNever()); $this->loop->removeWriteStream($input); @@ -79,7 +85,7 @@ public function testRemoveWriteStreamInstantly() public function testRemoveWriteStreamAfterWriting() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addWriteStream($input, $this->expectCallableOnce()); $this->loop->tick(); @@ -90,38 +96,60 @@ public function testRemoveWriteStreamAfterWriting() public function testRemoveStreamInstantly() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableNever()); $this->loop->addWriteStream($input, $this->expectCallableNever()); $this->loop->removeStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); + $this->loop->tick(); + } + + public function testRemoveStreamForReadOnly() + { + $input = $this->createStream(); + + $this->loop->addReadStream($input, $this->expectCallableNever()); + $this->loop->addWriteStream($input, $this->expectCallableOnce()); + $this->loop->removeReadStream($input); + + $this->writeToStream($input, "foo\n"); + $this->loop->tick(); + } + + public function testRemoveStreamForWriteOnly() + { + $input = $this->createStream(); + + $this->writeToStream($input, "foo\n"); + + $this->loop->addReadStream($input, $this->expectCallableOnce()); + $this->loop->addWriteStream($input, $this->expectCallableNever()); + $this->loop->removeWriteStream($input); + $this->loop->tick(); } public function testRemoveStream() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableOnce()); $this->loop->addWriteStream($input, $this->expectCallableOnce()); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); $this->loop->removeStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } public function testRemoveInvalid() { - $stream = fopen('php://temp', 'r+'); + $stream = $this->createStream(); // remove a valid stream from the event loop that was never added in the first place $this->loop->removeReadStream($stream); @@ -138,15 +166,14 @@ public function emptyRunShouldSimplyReturn() /** @test */ public function runShouldReturnWhenNoMoreFds() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $loop = $this->loop; $this->loop->addReadStream($input, function ($stream) use ($loop) { $loop->removeStream($stream); }); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->assertRunFasterThan(0.005); } @@ -154,15 +181,14 @@ public function runShouldReturnWhenNoMoreFds() /** @test */ public function stopShouldStopRunningLoop() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $loop = $this->loop; $this->loop->addReadStream($input, function ($stream) use ($loop) { $loop->stop(); }); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->assertRunFasterThan(0.005); } @@ -170,8 +196,8 @@ public function stopShouldStopRunningLoop() public function testIgnoreRemovedCallback() { // two independent streams, both should be readable right away - $stream1 = fopen('php://temp', 'r+'); - $stream2 = fopen('php://temp', 'r+'); + $stream1 = $this->createStream(); + $stream2 = $this->createStream(); $loop = $this->loop; $loop->addReadStream($stream1, function ($stream) use ($loop, $stream2) { @@ -179,20 +205,121 @@ public function testIgnoreRemovedCallback() $loop->removeReadStream($stream); $loop->removeReadStream($stream2); }); - $loop->addReadStream($stream2, function ($stream) use ($loop, $stream1) { - // this callback would have to be called as well, but the first stream already removed us - $loop->removeReadStream($stream); - $loop->removeReadStream($stream1); - }); - fwrite($stream1, "foo\n"); - rewind($stream1); - fwrite($stream2, "foo\n"); - rewind($stream2); + // this callback would have to be called as well, but the first stream already removed us + $loop->addReadStream($stream2, $this->expectCallableNever()); + + $this->writeToStream($stream1, "foo\n"); + $this->writeToStream($stream2, "foo\n"); $loop->run(); } + public function testNextTick() + { + $called = false; + + $callback = function ($loop) use (&$called) { + $this->assertSame($this->loop, $loop); + $called = true; + }; + + $this->loop->nextTick($callback); + + $this->assertFalse($called); + + $this->loop->tick(); + + $this->assertTrue($called); + } + + public function testNextTickFiresBeforeIO() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->loop->tick(); + } + + public function testRecursiveNextTick() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->nextTick( + function () { + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->loop->tick(); + } + + public function testRunWaitsForNextTickEvents() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () use ($stream) { + $this->loop->removeStream($stream); + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL); + + $this->loop->run(); + } + + public function testNextTickEventGeneratedByTimer() + { + $this->loop->addTimer( + 0.001, + function () { + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL); + + $this->loop->run(); + } + private function assertRunFasterThan($maxInterval) { $start = microtime(true); diff --git a/tests/React/Tests/EventLoop/LibEventLoopTest.php b/tests/React/Tests/EventLoop/LibEventLoopTest.php index 4b39b71f..920b33cc 100644 --- a/tests/React/Tests/EventLoop/LibEventLoopTest.php +++ b/tests/React/Tests/EventLoop/LibEventLoopTest.php @@ -6,9 +6,11 @@ class LibEventLoopTest extends AbstractLoopTest { + private $fifoPath; + public function createLoop() { - if ('Linux' === PHP_OS) { + if ('Linux' === PHP_OS && !extension_loaded('posix')) { $this->markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); } @@ -19,8 +21,38 @@ public function createLoop() return new LibEventLoop(); } - public function testLibEventConstructor() + public function tearDown() + { + if (file_exists($this->fifoPath)) { + unlink($this->fifoPath); + } + } + + public function createStream() + { + if ('Linux' !== PHP_OS) { + return parent::createStream(); + } + + $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); + + unlink($this->fifoPath); + + // Use a FIFO on linux to get around lack of support for disk-based file + // descriptors when using the EPOLL back-end. + posix_mkfifo($this->fifoPath, 0600); + + $stream = fopen($this->fifoPath, 'r+'); + + return $stream; + } + + public function writeToStream($stream, $content) { - $loop = new LibEventLoop(); + if ('Linux' !== PHP_OS) { + return parent::writeToStream($stream, $content); + } + + fwrite($stream, $content); } } diff --git a/tests/React/Tests/EventLoop/StreamSelectLoopTest.php b/tests/React/Tests/EventLoop/StreamSelectLoopTest.php index 71827600..55d3d165 100644 --- a/tests/React/Tests/EventLoop/StreamSelectLoopTest.php +++ b/tests/React/Tests/EventLoop/StreamSelectLoopTest.php @@ -11,8 +11,20 @@ public function createLoop() return new StreamSelectLoop(); } - public function testStreamSelectConstructor() + public function testStreamSelectTimeoutEmulation() { - $loop = new StreamSelectLoop(); + $this->loop->addTimer( + 0.05, + $this->expectCallableOnce() + ); + + $start = microtime(true); + + $this->loop->run(); + + $end = microtime(true); + $interval = $end - $start; + + $this->assertGreaterThan(0.04, $interval); } } diff --git a/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php b/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php index 8537e39c..5ff7bb29 100644 --- a/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php +++ b/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php @@ -73,4 +73,26 @@ public function testAddPeriodicTimerCancelsItself() $this->assertSame(2, $i); } + + public function testIsTimerActive() + { + $loop = $this->createLoop(); + + $timer = $loop->addPeriodicTimer(0.001, function () {}); + + $this->assertTrue($loop->isTimerActive($timer)); + + $timer->cancel(); + + $this->assertFalse($loop->isTimerActive($timer)); + } + + public function testMinimumIntervalOneMicrosecond() + { + $loop = $this->createLoop(); + + $timer = $loop->addTimer(0, function () {}); + + $this->assertEquals(0.000001, $timer->getInterval()); + } }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: