Skip to content

Implement ExtEvLoop (PECL ext-ev) #148

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

Merged
merged 4 commits into from
Apr 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -201,6 +202,16 @@ It supports the same backends as libevent.

This loop is known to work with PHP 5.4 through PHP 7+.

#### ExtEvLoop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also be added to the TOC above 👍


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.
Expand Down
252 changes: 252 additions & 0 deletions src/ExtEvLoop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php

namespace React\EventLoop;

use Ev;
use EvIo;
use EvLoop;
use React\EventLoop\Tick\FutureTickQueue;
use React\EventLoop\Timer\Timer;
use SplObjectStorage;

/**
* 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+.
*
* @see http://php.net/manual/en/book.ev.php
* @see https://bitbucket.org/osmanov/pecl-ev/overview
*/
class ExtEvLoop implements LoopInterface
{
/**
* @var EvLoop
*/
private $loop;

/**
* @var FutureTickQueue
*/
private $futureTickQueue;

/**
* @var SplObjectStorage
*/
private $timers;

/**
* @var EvIo[]
*/
private $readStreams = [];

/**
* @var EvIo[]
*/
private $writeStreams = [];

/**
* @var bool
*/
private $running;

/**
* @var SignalsHandler
*/
private $signals;

/**
* @var \EvSignal[]
*/
private $signalEvents = [];

public function __construct()
{
$this->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);

$that = $this;
$timers = $this->timers;
$callback = function () use ($timer, $timers, $that) {
call_user_func($timer->getCallback(), $timer);

if ($timers->contains($timer)) {
$that->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 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]);
}
}
}
2 changes: 2 additions & 0 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions tests/ExtEvLoopTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace React\Tests\EventLoop;

use React\EventLoop\ExtEvLoop;

class ExtEvLoopTest extends AbstractLoopTest
{
public function createLoop()
{
if (!class_exists('EvLoop')) {
$this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.');
}

return new ExtEvLoop();
}
}
4 changes: 4 additions & 0 deletions tests/Timer/AbstractTimerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
17 changes: 17 additions & 0 deletions tests/Timer/ExtEvTimerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace React\Tests\EventLoop\Timer;

use React\EventLoop\ExtEvLoop;

class ExtEvTimerTest extends AbstractTimerTest
{
public function createLoop()
{
if (!class_exists('EvLoop')) {
$this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.');
}

return new ExtEvLoop();
}
}
3 changes: 2 additions & 1 deletion travis-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ 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 'libevent' PHP extension (does not support php 7)
Expand Down