From b325a88cc8ddd14594fa6fbc92cc73f7bf97ae6f Mon Sep 17 00:00:00 2001 From: Ivan Kalita Date: Mon, 15 Jan 2018 23:48:11 +0300 Subject: [PATCH 1/4] Implement ExtEvLoop. ExtEvLoop implements event loop based on PECL ev extension. --- README.md | 10 ++ src/ExtEvLoop.php | 255 ++++++++++++++++++++++++++++++ src/Factory.php | 2 + tests/ExtEvLoopTest.php | 17 ++ tests/Timer/AbstractTimerTest.php | 4 + tests/Timer/ExtEvTimerTest.php | 17 ++ travis-init.sh | 3 + 7 files changed, 308 insertions(+) create mode 100644 src/ExtEvLoop.php create mode 100644 tests/ExtEvLoopTest.php create mode 100644 tests/Timer/ExtEvTimerTest.php diff --git a/README.md b/README.md index 7a30ec15..b2a47887 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,16 @@ It supports the same backends as libevent. This loop is known to work with PHP 5.4 through PHP 7+. +#### ExtEvLoop + +An `ext-ev` based event loop. + +This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev), that +provides an interface to `libev` library. + +This loop is known to work with PHP 5.4 through PHP 7+. + + #### ExtLibeventLoop An `ext-libevent` based event loop. diff --git a/src/ExtEvLoop.php b/src/ExtEvLoop.php new file mode 100644 index 00000000..2be45c34 --- /dev/null +++ b/src/ExtEvLoop.php @@ -0,0 +1,255 @@ +loop = new EvLoop(); + $this->futureTickQueue = new FutureTickQueue(); + $this->timers = new SplObjectStorage(); + $this->signals = new SignalsHandler(); + } + + public function addReadStream($stream, $listener) + { + $key = (int)$stream; + + if (isset($this->readStreams[$key])) { + return; + } + + $callback = $this->getStreamListenerClosure($stream, $listener); + $event = $this->loop->io($stream, Ev::READ, $callback); + $this->readStreams[$key] = $event; + } + + /** + * @param resource $stream + * @param callable $listener + * + * @return \Closure + */ + private function getStreamListenerClosure($stream, $listener) + { + return function () use ($stream, $listener) { + call_user_func($listener, $stream); + }; + } + + public function addWriteStream($stream, $listener) + { + $key = (int)$stream; + + if (isset($this->writeStreams[$key])) { + return; + } + + $callback = $this->getStreamListenerClosure($stream, $listener); + $event = $this->loop->io($stream, Ev::WRITE, $callback); + $this->writeStreams[$key] = $event; + } + + public function removeReadStream($stream) + { + $key = (int)$stream; + + if (!isset($this->readStreams[$key])) { + return; + } + + $this->readStreams[$key]->stop(); + unset($this->readStreams[$key]); + } + + public function removeWriteStream($stream) + { + $key = (int)$stream; + + if (!isset($this->writeStreams[$key])) { + return; + } + + $this->writeStreams[$key]->stop(); + unset($this->writeStreams[$key]); + } + + public function addTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, false); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + + if ($this->isTimerActive($timer)) { + $this->cancelTimer($timer); + } + }; + + $event = $this->loop->timer($timer->getInterval(), 0.0, $callback); + $this->timers->attach($timer, $event); + + return $timer; + } + + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, true); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + }; + + $event = $this->loop->timer($interval, $interval, $callback); + $this->timers->attach($timer, $event); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + if (!isset($this->timers[$timer])) { + return; + } + + $event = $this->timers[$timer]; + $event->stop(); + $this->timers->detach($timer); + } + + public function isTimerActive(TimerInterface $timer) + { + return $this->timers->contains($timer); + } + + public function futureTick($listener) + { + $this->futureTickQueue->add($listener); + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $hasPendingCallbacks = !$this->futureTickQueue->isEmpty(); + $wasJustStopped = !$this->running; + $nothingLeftToDo = !$this->readStreams + && !$this->writeStreams + && !$this->timers->count() + && $this->signals->isEmpty(); + + $flags = Ev::RUN_ONCE; + if ($wasJustStopped || $hasPendingCallbacks) { + $flags |= Ev::RUN_NOWAIT; + } elseif ($nothingLeftToDo) { + break; + } + + $this->loop->run($flags); + } + } + + public function stop() + { + $this->running = false; + } + + public function __destruct() + { + /** @var TimerInterface $timer */ + foreach ($this->timers as $timer) { + $this->cancelTimer($timer); + } + + foreach ($this->readStreams as $key => $stream) { + $this->removeReadStream($key); + } + + foreach ($this->writeStreams as $key => $stream) { + $this->removeWriteStream($key); + } + } + + public function addSignal($signal, $listener) + { + $this->signals->add($signal, $listener); + + if (!isset($this->signalEvents[$signal])) { + $this->signalEvents[$signal] = $this->loop->signal($signal, function() use ($signal) { + $this->signals->call($signal); + }); + } + } + + public function removeSignal($signal, $listener) + { + $this->signals->remove($signal, $listener); + + if (isset($this->signalEvents[$signal])) { + $this->signalEvents[$signal]->stop(); + unset($this->signalEvents[$signal]); + } + } +} diff --git a/src/Factory.php b/src/Factory.php index 1a56877d..b46fc074 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -26,6 +26,8 @@ public static function create() // @codeCoverageIgnoreStart if (class_exists('libev\EventLoop', false)) { return new ExtLibevLoop(); + } elseif (class_exists('EvLoop', false)) { + return new ExtEvLoop(); } elseif (class_exists('EventBase', false)) { return new ExtEventLoop(); } elseif (function_exists('event_base_new') && PHP_VERSION_ID < 70000) { diff --git a/tests/ExtEvLoopTest.php b/tests/ExtEvLoopTest.php new file mode 100644 index 00000000..ab41c9f3 --- /dev/null +++ b/tests/ExtEvLoopTest.php @@ -0,0 +1,17 @@ +markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.'); + } + + return new ExtEvLoop(); + } +} diff --git a/tests/Timer/AbstractTimerTest.php b/tests/Timer/AbstractTimerTest.php index 15202e41..294e683f 100644 --- a/tests/Timer/AbstractTimerTest.php +++ b/tests/Timer/AbstractTimerTest.php @@ -2,10 +2,14 @@ namespace React\Tests\EventLoop\Timer; +use React\EventLoop\LoopInterface; use React\Tests\EventLoop\TestCase; abstract class AbstractTimerTest extends TestCase { + /** + * @return LoopInterface + */ abstract public function createLoop(); public function testAddTimerReturnsNonPeriodicTimerInstance() diff --git a/tests/Timer/ExtEvTimerTest.php b/tests/Timer/ExtEvTimerTest.php new file mode 100644 index 00000000..bfa91861 --- /dev/null +++ b/tests/Timer/ExtEvTimerTest.php @@ -0,0 +1,17 @@ +markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.'); + } + + return new ExtEvLoop(); + } +} diff --git a/travis-init.sh b/travis-init.sh index 06f853fe..c835d0e6 100755 --- a/travis-init.sh +++ b/travis-init.sh @@ -10,6 +10,9 @@ if [[ "$TRAVIS_PHP_VERSION" != "hhvm" && echo "yes" | pecl install event fi + # install 'ev' PHP extension + echo "yes" | pecl install ev + # install 'libevent' PHP extension (does not support php 7) if [[ "$TRAVIS_PHP_VERSION" != "7.0" && "$TRAVIS_PHP_VERSION" != "7.1" && From 43dc6aca1000f2b1fb0f3769dc7820a4b1822bbe Mon Sep 17 00:00:00 2001 From: Ivan Kalita Date: Wed, 28 Feb 2018 20:52:15 +0300 Subject: [PATCH 2/4] Fix travis: skip ev extension installation if PHP version is 5.3. --- travis-init.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/travis-init.sh b/travis-init.sh index c835d0e6..29ce884a 100755 --- a/travis-init.sh +++ b/travis-init.sh @@ -5,14 +5,12 @@ set -o pipefail if [[ "$TRAVIS_PHP_VERSION" != "hhvm" && "$TRAVIS_PHP_VERSION" != "hhvm-nightly" ]]; then - # install 'event' PHP extension + # install 'event' and 'ev' PHP extension if [[ "$TRAVIS_PHP_VERSION" != "5.3" ]]; then echo "yes" | pecl install event + echo "yes" | pecl install ev fi - # install 'ev' PHP extension - echo "yes" | pecl install ev - # install 'libevent' PHP extension (does not support php 7) if [[ "$TRAVIS_PHP_VERSION" != "7.0" && "$TRAVIS_PHP_VERSION" != "7.1" && From 882597b2c966a2bf236628942e37495830766c88 Mon Sep 17 00:00:00 2001 From: Ivan Kalita Date: Fri, 6 Apr 2018 18:28:34 +0300 Subject: [PATCH 3/4] Remove ExtEvLoop::isTimerActive. --- src/ExtEvLoop.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/ExtEvLoop.php b/src/ExtEvLoop.php index 2be45c34..74db6d02 100644 --- a/src/ExtEvLoop.php +++ b/src/ExtEvLoop.php @@ -137,11 +137,13 @@ public function addTimer($interval, $callback) { $timer = new Timer($interval, $callback, false); - $callback = function () use ($timer) { + $that = $this; + $timers = $this->timers; + $callback = function () use ($timer, $timers, $that) { call_user_func($timer->getCallback(), $timer); - if ($this->isTimerActive($timer)) { - $this->cancelTimer($timer); + if ($timers->contains($timer)) { + $that->cancelTimer($timer); } }; @@ -176,11 +178,6 @@ public function cancelTimer(TimerInterface $timer) $this->timers->detach($timer); } - public function isTimerActive(TimerInterface $timer) - { - return $this->timers->contains($timer); - } - public function futureTick($listener) { $this->futureTickQueue->add($listener); From cedf94181bb35774a593b2543b1294ead750e268 Mon Sep 17 00:00:00 2001 From: Ivan Kalita Date: Fri, 6 Apr 2018 18:37:13 +0300 Subject: [PATCH 4/4] Fix README: add ExtEvLoop to TOC. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b2a47887..65f4695f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ For the code of the current stable 0.4.x release, checkout the * [ExtEventLoop](#exteventloop) * [ExtLibeventLoop](#extlibeventloop) * [ExtLibevLoop](#extlibevloop) + * [ExtEvLoop](#extevloop) * [LoopInterface](#loopinterface) * [addTimer()](#addtimer) * [addPeriodicTimer()](#addperiodictimer)