Skip to content

Commit 405cb25

Browse files
ivkalitaIvan Kalita
authored andcommitted
Implement ExtEvLoop.
ExtEvLoop implements event loop based on PECL ev extension.
1 parent 502b4b6 commit 405cb25

File tree

8 files changed

+278
-3
lines changed

8 files changed

+278
-3
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ It supports the same backends as libevent.
201201

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

204+
#### ExtEvLoop
205+
206+
An `ext-ev` based event loop.
207+
208+
This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev), that
209+
provides an interface to `libev` library.
210+
211+
This loop is known to work with PHP 5.4 through PHP 7+.
212+
213+
204214
#### ExtLibeventLoop
205215

206216
An `ext-libevent` based event loop.

src/ExtEvLoop.php

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
3+
namespace React\EventLoop;
4+
5+
use Ev;
6+
use EvLoop;
7+
use React\EventLoop\Tick\FutureTickQueue;
8+
use React\EventLoop\Timer\Timer;
9+
use SplObjectStorage;
10+
11+
/**
12+
* An `ext-ev` based event loop.
13+
*
14+
* This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev),
15+
* that provides an interface to `libev` library.
16+
*
17+
* This loop is known to work with PHP 5.4 through PHP 7+.
18+
*
19+
* @see http://php.net/manual/en/book.ev.php
20+
* @see https://bitbucket.org/osmanov/pecl-ev/overview
21+
*/
22+
class ExtEvLoop implements LoopInterface
23+
{
24+
private $loop;
25+
private $futureTickQueue;
26+
private $timers;
27+
private $readStreams = [];
28+
private $writeStreams = [];
29+
private $running;
30+
private $signals;
31+
private $signalEvents = [];
32+
33+
public function __construct()
34+
{
35+
$this->loop = new EvLoop();
36+
$this->futureTickQueue = new FutureTickQueue();
37+
$this->timers = new SplObjectStorage();
38+
$this->signals = new SignalsHandler(
39+
$this,
40+
function ($signal) {
41+
$this->signalEvents[$signal] = $this->loop->signal($signal, function () use ($signal) {
42+
$this->signals->call($signal);
43+
});
44+
},
45+
function ($signal) {
46+
if ($this->signals->count($signal) === 0) {
47+
unset($this->signalEvents[$signal]);
48+
}
49+
}
50+
);
51+
}
52+
53+
public function addReadStream($stream, $listener)
54+
{
55+
$key = (int)$stream;
56+
57+
if (isset($this->readStreams[$key])) {
58+
return;
59+
}
60+
61+
$callback = $this->getStreamListenerClosure($stream, $listener);
62+
$event = $this->loop->io($stream, Ev::READ, $callback);
63+
$this->readStreams[$key] = $event;
64+
}
65+
66+
/**
67+
* @param resource $stream
68+
* @param callable $listener
69+
*
70+
* @return \Closure
71+
*/
72+
private function getStreamListenerClosure($stream, callable $listener)
73+
{
74+
return function () use ($stream, $listener) {
75+
call_user_func($listener, $stream, $this);
76+
};
77+
}
78+
79+
public function addWriteStream($stream, $listener)
80+
{
81+
$key = (int)$stream;
82+
83+
if (isset($this->writeStreams[$key])) {
84+
return;
85+
}
86+
87+
$callback = $this->getStreamListenerClosure($stream, $listener);
88+
$event = $this->loop->io($stream, Ev::WRITE, $callback);
89+
$this->writeStreams[$key] = $event;
90+
}
91+
92+
public function removeReadStream($stream)
93+
{
94+
$key = (int)$stream;
95+
96+
if (!isset($this->readStreams[$key])) {
97+
return;
98+
}
99+
100+
$this->readStreams[$key]->stop();
101+
unset($this->readStreams[$key]);
102+
}
103+
104+
public function removeWriteStream($stream)
105+
{
106+
$key = (int)$stream;
107+
108+
if (!isset($this->writeStreams[$key])) {
109+
return;
110+
}
111+
112+
$this->writeStreams[$key]->stop();
113+
unset($this->writeStreams[$key]);
114+
}
115+
116+
public function addTimer($interval, $callback)
117+
{
118+
$timer = new Timer($interval, $callback, false);
119+
120+
$callback = function () use ($timer) {
121+
call_user_func($timer->getCallback(), $timer);
122+
123+
if ($this->isTimerActive($timer)) {
124+
$this->cancelTimer($timer);
125+
}
126+
};
127+
128+
$event = $this->loop->timer($timer->getInterval(), 0.0, $callback);
129+
$this->timers->attach($timer, $event);
130+
131+
return $timer;
132+
}
133+
134+
public function addPeriodicTimer($interval, $callback)
135+
{
136+
$timer = new Timer($interval, $callback, true);
137+
138+
$callback = function () use ($timer) {
139+
call_user_func($timer->getCallback(), $timer);
140+
};
141+
142+
$event = $this->loop->timer($interval, $interval, $callback);
143+
$this->timers->attach($timer, $event);
144+
145+
return $timer;
146+
}
147+
148+
public function cancelTimer(TimerInterface $timer)
149+
{
150+
if (!isset($this->timers[$timer])) {
151+
return;
152+
}
153+
154+
$event = $this->timers[$timer];
155+
$event->stop();
156+
$this->timers->detach($timer);
157+
}
158+
159+
public function isTimerActive(TimerInterface $timer)
160+
{
161+
return $this->timers->contains($timer);
162+
}
163+
164+
public function futureTick($listener)
165+
{
166+
$this->futureTickQueue->add($listener);
167+
}
168+
169+
public function run()
170+
{
171+
$this->running = true;
172+
173+
while ($this->running) {
174+
$this->futureTickQueue->tick();
175+
176+
$hasPendingCallbacks = !$this->futureTickQueue->isEmpty();
177+
$wasJustStopped = !$this->running;
178+
$nothingLeftToDo = !$this->readStreams && !$this->writeStreams && !$this->timers->count();
179+
180+
$flags = Ev::RUN_ONCE;
181+
if ($wasJustStopped || $hasPendingCallbacks) {
182+
$flags |= Ev::RUN_NOWAIT;
183+
} elseif ($nothingLeftToDo) {
184+
break;
185+
}
186+
187+
$this->loop->run($flags);
188+
}
189+
}
190+
191+
public function stop()
192+
{
193+
$this->running = false;
194+
}
195+
196+
public function __destruct()
197+
{
198+
/** @var TimerInterface $timer */
199+
foreach ($this->timers as $timer) {
200+
$this->cancelTimer($timer);
201+
}
202+
203+
foreach ($this->readStreams as $key => $stream) {
204+
$this->removeReadStream($key);
205+
}
206+
207+
foreach ($this->writeStreams as $key => $stream) {
208+
$this->removeWriteStream($key);
209+
}
210+
}
211+
212+
public function addSignal($signal, $listener)
213+
{
214+
$this->signals->add($signal, $listener);
215+
}
216+
217+
public function removeSignal($signal, $listener)
218+
{
219+
$this->signals->remove($signal, $listener);
220+
}
221+
}

src/Factory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ public static function create()
2626
// @codeCoverageIgnoreStart
2727
if (class_exists('libev\EventLoop', false)) {
2828
return new ExtLibevLoop();
29-
} elseif (class_exists('EventBase', false)) {
29+
} elseif (class_exists('EvLoop', false)) {
30+
return new ExtEvLoop();
31+
}
32+
elseif (class_exists('EventBase', false)) {
3033
return new ExtEventLoop();
3134
} elseif (function_exists('event_base_new') && PHP_VERSION_ID < 70000) {
3235
// only use ext-libevent on PHP < 7 for now

tests/AbstractLoopTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,12 +534,12 @@ public function testSignalsKeepTheLoopRunning()
534534
{
535535
$function = function () {};
536536
$this->loop->addSignal(SIGUSR1, $function);
537-
$this->loop->addTimer(1.5, function () use ($function) {
537+
$this->loop->addTimer(2, function () use ($function) {
538538
$this->loop->removeSignal(SIGUSR1, $function);
539539
$this->loop->stop();
540540
});
541541

542-
$this->assertRunSlowerThan(1.5);
542+
$this->assertRunSlowerThan(2);
543543
}
544544

545545
public function testSignalsKeepTheLoopRunningAndRemovingItStopsTheLoop()

tests/ExtEvLoopTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace React\Tests\EventLoop;
4+
5+
use React\EventLoop\ExtEvLoop;
6+
7+
class ExtEvLoopTest extends AbstractLoopTest
8+
{
9+
public function createLoop()
10+
{
11+
if (!class_exists('EvLoop')) {
12+
$this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.');
13+
}
14+
15+
return new ExtEvLoop();
16+
}
17+
}

tests/Timer/AbstractTimerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace React\Tests\EventLoop\Timer;
44

5+
use React\EventLoop\LoopInterface;
56
use React\Tests\EventLoop\TestCase;
67

78
abstract class AbstractTimerTest extends TestCase
89
{
10+
/**
11+
* @return LoopInterface
12+
*/
913
abstract public function createLoop();
1014

1115
public function testAddTimerReturnsNonPeriodicTimerInstance()

tests/Timer/ExtEvTimerTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace React\Tests\EventLoop\Timer;
4+
5+
use React\EventLoop\ExtEvLoop;
6+
7+
class ExtEvTimerTest extends AbstractTimerTest
8+
{
9+
public function createLoop()
10+
{
11+
if (!class_exists('EvLoop')) {
12+
$this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.');
13+
}
14+
15+
return new ExtEvLoop();
16+
}
17+
}

travis-init.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ if [[ "$TRAVIS_PHP_VERSION" != "hhvm" &&
88
# install 'event' PHP extension
99
echo "yes" | pecl install event
1010

11+
# install 'ev' PHP extension
12+
echo "yes" | pecl install ev
13+
1114
# install 'libevent' PHP extension (does not support php 7)
1215
if [[ "$TRAVIS_PHP_VERSION" != "7.0" &&
1316
"$TRAVIS_PHP_VERSION" != "7.1" &&

0 commit comments

Comments
 (0)