Skip to content

Commit 3722eda

Browse files
committed
UV Event Loop
1 parent 49ac483 commit 3722eda

File tree

6 files changed

+320
-2
lines changed

6 files changed

+320
-2
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ addons:
2121
apt:
2222
packages:
2323
- libevent-dev # Used by 'event' and 'libevent' PHP extensions
24+
- libuv-dev # Used by 'uv' PHP extensions
2425

2526
cache:
2627
directories:
2728
- $HOME/.composer/cache/files
2829

30+
before_install:
31+
- sudo add-apt-repository ppa:ondrej/php -y
32+
- sudo apt-get update -q
33+
- sudo apt-get install libuv1-dev
34+
2935
install:
3036
- ./travis-init.sh
3137
- composer install

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"ext-libevent": ">=0.1.0 for LibEventLoop and PHP5 only",
1414
"ext-event": "~1.0 for ExtEventLoop",
1515
"ext-libev": "for LibEvLoop",
16-
"ext-pcntl": "For signals support when using the stream_select loop"
16+
"ext-pcntl": "For signals support when using the stream_select loop",
17+
"ext-libuv": "*"
1718
},
1819
"autoload": {
1920
"psr-4": {

src/Factory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ class Factory
2424
public static function create()
2525
{
2626
// @codeCoverageIgnoreStart
27-
if (class_exists('libev\EventLoop', false)) {
27+
if (function_exists('uv_default_loop')) {
28+
return new LibUvLoop();
29+
} elseif (class_exists('libev\EventLoop', false)) {
2830
return new LibEvLoop;
2931
} elseif (class_exists('EventBase', false)) {
3032
return new ExtEventLoop;

src/LibUvLoop.php

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
<?php
2+
3+
namespace React\EventLoop;
4+
5+
use React\EventLoop\Tick\FutureTickQueue;
6+
use React\EventLoop\Timer\Timer;
7+
use React\EventLoop\Timer\TimerInterface;
8+
use SplObjectStorage;
9+
10+
/**
11+
* @see https://github.com/bwoebi/php-uv
12+
*/
13+
class LibUvLoop implements LoopInterface
14+
{
15+
private $uv;
16+
private $futureTickQueue;
17+
private $timerEvents;
18+
private $events = [];
19+
private $flags = [];
20+
private $listeners = [];
21+
private $running;
22+
private $streamListener;
23+
24+
public function __construct()
25+
{
26+
$this->uv = \uv_loop_new();
27+
$this->futureTickQueue = new FutureTickQueue();
28+
$this->timerEvents = new SplObjectStorage();
29+
$this->streamListener = $this->createStreamListener();
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function addReadStream($stream, callable $listener)
36+
{
37+
if (isset($this->listeners[(int) $stream]['read'])) {
38+
return;
39+
}
40+
41+
$this->listeners[(int) $stream]['read'] = $listener;
42+
$this->addStream($stream);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function addWriteStream($stream, callable $listener)
49+
{
50+
if (isset($this->listeners[(int) $stream]['write'])) {
51+
return;
52+
}
53+
54+
$this->listeners[(int) $stream]['write'] = $listener;
55+
$this->addStream($stream);
56+
}
57+
58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function removeReadStream($stream)
62+
{
63+
if (!isset($this->events[(int) $stream])) {
64+
return;
65+
}
66+
67+
unset($this->listeners[(int) $stream]['read']);
68+
69+
$this->____removeStream($stream);
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function removeWriteStream($stream)
76+
{
77+
if (!isset($this->events[(int) $stream])) {
78+
return;
79+
}
80+
81+
unset($this->listeners[(int) $stream]['write']);
82+
83+
$this->____removeStream($stream);
84+
}
85+
86+
/**
87+
* {@inheritdoc}
88+
*/
89+
public function removeStream($stream)
90+
{
91+
if (isset($this->events[(int) $stream])) {
92+
unset($this->listeners[(int) $stream]['read']);
93+
unset($this->listeners[(int) $stream]['write']);
94+
95+
$this->____removeStream($stream);
96+
}
97+
}
98+
99+
/**
100+
* {@inheritdoc}
101+
*/
102+
public function addTimer($interval, callable $callback)
103+
{
104+
$timer = new Timer( $interval, $callback, false);
105+
106+
$callback = function () use ($timer) {
107+
call_user_func($timer->getCallback(), $timer);
108+
109+
if ($this->isTimerActive($timer)) {
110+
$this->cancelTimer($timer);
111+
}
112+
};
113+
114+
$event = \uv_timer_init($this->uv);
115+
$this->timerEvents->attach($timer, $event);
116+
\uv_timer_start(
117+
$event,
118+
$interval * 1000,
119+
0,
120+
$callback
121+
);
122+
123+
return $timer;
124+
}
125+
126+
/**
127+
* {@inheritdoc}
128+
*/
129+
public function addPeriodicTimer($interval, callable $callback)
130+
{
131+
$timer = new Timer($interval, $callback, true);
132+
133+
$callback = function () use ($timer) {
134+
call_user_func($timer->getCallback(), $timer);
135+
};
136+
137+
$event = \uv_timer_init($this->uv);
138+
$this->timerEvents->attach($timer, $event);
139+
\uv_timer_start(
140+
$event,
141+
$interval * 1000,
142+
$interval * 1000,
143+
$callback
144+
);
145+
146+
return $timer;
147+
}
148+
149+
/**
150+
* {@inheritdoc}
151+
*/
152+
public function cancelTimer(TimerInterface $timer)
153+
{
154+
if (isset($this->timerEvents[$timer])) {
155+
@\uv_timer_stop($this->timerEvents[$timer]);
156+
$this->timerEvents->detach($timer);
157+
}
158+
}
159+
160+
/**
161+
* {@inheritdoc}
162+
*/
163+
public function isTimerActive(TimerInterface $timer)
164+
{
165+
return $this->timerEvents->contains($timer);
166+
}
167+
168+
/**
169+
* {@inheritdoc}
170+
*/
171+
public function futureTick(callable $listener)
172+
{
173+
$this->futureTickQueue->add($listener);
174+
}
175+
176+
/**
177+
* {@inheritdoc}
178+
*/
179+
public function run()
180+
{
181+
$this->running = true;
182+
183+
while ($this->running) {
184+
$this->futureTickQueue->tick();
185+
186+
if ($this->futureTickQueue->isEmpty() && empty($this->events) && $this->timerEvents->count() === 0) {
187+
break;
188+
}
189+
190+
\uv_run($this->uv, \UV::RUN_NOWAIT);
191+
}
192+
}
193+
194+
/**
195+
* {@inheritdoc}
196+
*/
197+
public function stop()
198+
{
199+
$this->running = false;
200+
}
201+
202+
private function addStream($stream)
203+
{
204+
// Run in tick or else things epically fail with loop->watchers[w->fd] == w
205+
$this->futureTick(function () use ($stream) {
206+
if (!isset($this->events[(int) $stream])) {
207+
$this->events[(int) $stream] = \uv_poll_init_socket($this->uv, $stream);
208+
}
209+
210+
$this->pollStream($stream);
211+
});
212+
}
213+
214+
// To do: get latest changes in from react:master so we can use this method name internally
215+
private function ____removeStream($stream)
216+
{
217+
// Run in tick or else things epically fail with loop->watchers[w->fd] == w
218+
$this->futureTick(function () use ($stream) {
219+
if (!isset($this->events[(int) $stream])) {
220+
return;
221+
}
222+
223+
if (!isset($this->listeners[(int) $stream]['read'])
224+
&& !isset($this->listeners[(int) $stream]['write'])) {
225+
\uv_poll_stop($this->events[(int) $stream]);
226+
unset($this->events[(int) $stream]);
227+
unset($this->flags[(int) $stream]);
228+
return;
229+
}
230+
231+
$this->pollStream($stream);
232+
});
233+
}
234+
235+
private function pollStream($stream)
236+
{
237+
$flags = 0;
238+
if (isset($this->listeners[(int) $stream]['read'])) {
239+
$flags |= \UV::READABLE;
240+
}
241+
242+
if (isset($this->listeners[(int) $stream]['write'])) {
243+
$flags |= \UV::WRITABLE;
244+
}
245+
246+
if (isset($this->flags[(int) $stream]) && $this->flags[(int) $stream] == $flags) {
247+
return;
248+
}
249+
250+
$this->flags[(int) $stream] = $flags;
251+
252+
\uv_poll_start($this->events[(int) $stream], $flags, $this->streamListener);
253+
}
254+
255+
/**
256+
* Create a stream listener
257+
*
258+
* @return callable Returns a callback
259+
*/
260+
private function createStreamListener()
261+
{
262+
$callback = function ($event, $status, $events, $stream) {
263+
if ($status !== 0) {
264+
unset($this->flags[(int) $stream]);
265+
$this->pollStream($stream);
266+
}
267+
268+
if (isset($this->listeners[(int) $stream]['read']) && $events & \UV::READABLE) {
269+
call_user_func($this->listeners[(int) $stream]['read'], $stream);
270+
}
271+
272+
if (isset($this->listeners[(int) $stream]['write']) && $events & \UV::WRITABLE) {
273+
call_user_func($this->listeners[(int) $stream]['write'], $stream);
274+
}
275+
};
276+
277+
return $callback;
278+
}
279+
}

tests/LibUvLoopTest.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\LibUvLoop;
6+
7+
class LibUvLoopTest extends AbstractLoopTest
8+
{
9+
public function createLoop()
10+
{
11+
if (!function_exists('uv_default_loop')) {
12+
$this->markTestSkipped('libuv tests skipped because ext-libuv is not installed.');
13+
}
14+
15+
return new LibUvLoop();
16+
}
17+
}

travis-init.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,17 @@ if [[ "$TRAVIS_PHP_VERSION" != "hhvm" &&
3434
echo "extension=libev.so" >> "$(php -r 'echo php_ini_loaded_file();')"
3535
fi
3636

37+
# install 'libuv' PHP extension (does not support php 5)
38+
if [[ "$TRAVIS_PHP_VERSION" = "7.0" ||
39+
"$TRAVIS_PHP_VERSION" = "7.1" ]]; then
40+
git clone --recursive https://github.com/bwoebi/php-uv
41+
pushd php-uv
42+
phpize
43+
./configure
44+
make
45+
make install
46+
popd
47+
echo "extension=uv.so" >> "$(php -r 'echo php_ini_loaded_file();')"
48+
fi
49+
3750
fi

0 commit comments

Comments
 (0)