Skip to content

Commit 30441ec

Browse files
committed
[EventLoop] Implement timers for the stream_select() based loop.
Timers can be one-shot or periodic and both are cancellable by using the signature returned when adding them to the event loop. Periodic timers can be cancelled also by returing FALSE from the provided callback. The event loop will continue to run even without any registered stream until there is at least one timer scheduled for execution or the loop is explicitly stopped.
1 parent 9044dc0 commit 30441ec

File tree

2 files changed

+160
-15
lines changed

2 files changed

+160
-15
lines changed

StreamSelectLoop.php

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22

33
namespace React\EventLoop;
44

5+
use React\EventLoop\Timer\Timers;
6+
57
class StreamSelectLoop implements LoopInterface
68
{
7-
private $timeout;
9+
const QUANTUM_INTERVAL = 1000000;
810

11+
private $timers;
12+
private $running = false;
913
private $readStreams = array();
1014
private $readListeners = array();
11-
1215
private $writeStreams = array();
1316
private $writeListeners = array();
1417

15-
private $stopped = false;
16-
17-
// timeout = microseconds
18-
public function __construct($timeout = 1000000)
18+
public function __construct()
1919
{
20-
$this->timeout = $timeout;
20+
$this->timers = new Timers();
2121
}
2222

2323
public function addReadStream($stream, $listener)
@@ -66,17 +66,61 @@ public function removeStream($stream)
6666
$this->removeWriteStream($stream);
6767
}
6868

69-
public function tick()
69+
public function addTimer($interval, $callback)
70+
{
71+
return $this->timers->add($interval, $callback);
72+
}
73+
74+
public function addPeriodicTimer($interval, $callback)
75+
{
76+
return $this->timers->add($interval, $callback, true);
77+
}
78+
79+
public function cancelTimer($signature)
80+
{
81+
$this->timers->cancel($signature);
82+
}
83+
84+
protected function getNextEventTime()
85+
{
86+
$nextEvent = $this->timers->getFirst();
87+
88+
if ($nextEvent === -1) {
89+
return self::QUANTUM_INTERVAL;
90+
}
91+
92+
$currentTime = microtime(true);
93+
if ($nextEvent > $currentTime) {
94+
return ($nextEvent - $currentTime) * 1000000;
95+
}
96+
97+
return 0;
98+
}
99+
100+
protected function sleepOnPendingTimers()
101+
{
102+
if ($this->timers->isEmpty()) {
103+
$this->running = false;
104+
} else {
105+
// We use usleep() instead of stream_select() to emulate timeouts
106+
// since the latter fails when there are no streams registered for
107+
// read / write events. Blame PHP for us needing this hack.
108+
usleep($this->getNextEventTime());
109+
}
110+
}
111+
112+
protected function runStreamSelect()
70113
{
71114
$read = $this->readStreams ?: null;
72115
$write = $this->writeStreams ?: null;
73-
$excepts = null;
116+
$except = null;
74117

75118
if (!$read && !$write) {
76-
return false;
119+
$this->sleepOnPendingTimers();
120+
return;
77121
}
78122

79-
if (stream_select($read, $write, $except, 0, $this->timeout) > 0) {
123+
if (stream_select($read, $write, $except, 0, $this->getNextEventTime()) > 0) {
80124
if ($read) {
81125
foreach ($read as $stream) {
82126
$listener = $this->readListeners[(int) $stream];
@@ -99,16 +143,22 @@ public function tick()
99143
}
100144
}
101145
}
146+
}
147+
148+
public function tick()
149+
{
150+
$this->timers->run();
151+
$this->runStreamSelect();
102152

103-
return true;
153+
return $this->running;
104154
}
105155

106156
public function run()
107157
{
108158
// @codeCoverageIgnoreStart
109-
$this->stopped = false;
159+
$this->running = true;
110160

111-
while ($this->tick() === true && !$this->stopped) {
161+
while ($this->tick()) {
112162
// NOOP
113163
}
114164
// @codeCoverageIgnoreEnd
@@ -117,7 +167,7 @@ public function run()
117167
public function stop()
118168
{
119169
// @codeCoverageIgnoreStart
120-
$this->stopped = true;
170+
$this->running = false;
121171
// @codeCoverageIgnoreEnd
122172
}
123173
}

Timer/Timers.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace React\EventLoop\Timer;
4+
5+
class Timers
6+
{
7+
const MIN_RESOLUTION = 0.001;
8+
9+
private $time;
10+
private $active;
11+
private $timers;
12+
13+
public function __construct()
14+
{
15+
$this->time = 0;
16+
$this->active = array();
17+
$this->timers = new \SplPriorityQueue();
18+
}
19+
20+
public function updateTime()
21+
{
22+
return $this->time = microtime(true);
23+
}
24+
25+
public function getTime()
26+
{
27+
return $this->time ?: $this->updateTime();
28+
}
29+
30+
public function add($interval, $callback, $periodic = false)
31+
{
32+
if ($interval < self::MIN_RESOLUTION) {
33+
throw new \InvalidArgumentException('Timer events do not support sub-millisecond timeouts.');
34+
}
35+
36+
if (!is_callable($callback)) {
37+
throw new \InvalidArgumentException('The callback must be a callable object.');
38+
}
39+
40+
$interval = (float) $interval;
41+
42+
$timer = (object) array(
43+
'interval' => $interval,
44+
'callback' => $callback,
45+
'periodic' => $periodic,
46+
'scheduled' => $interval + $this->getTime(),
47+
);
48+
49+
$signature = $timer->signature = spl_object_hash($timer);
50+
$this->timers->insert($timer, -$timer->scheduled);
51+
$this->active[$signature] = $timer;
52+
53+
return $signature;
54+
}
55+
56+
public function cancel($signature)
57+
{
58+
unset($this->active[$signature]);
59+
}
60+
61+
public function getFirst()
62+
{
63+
if ($this->timers->isEmpty()) {
64+
return -1;
65+
}
66+
67+
return $this->timers->top()->scheduled;
68+
}
69+
70+
public function isEmpty()
71+
{
72+
return !$this->active;
73+
}
74+
75+
public function run()
76+
{
77+
$time = $this->updateTime();
78+
$timers = $this->timers;
79+
80+
while (!$timers->isEmpty() && $timers->top()->scheduled < $time) {
81+
$timer = $timers->extract();
82+
83+
if (isset($this->active[$timer->signature])) {
84+
$rearm = call_user_func($timer->callback);
85+
86+
if ($timer->periodic === true && $rearm !== false) {
87+
$timer->scheduled = $timer->interval + $time;
88+
$this->timers->insert($timer, -$timer->scheduled);
89+
} else {
90+
unset($this->active[$timer->signature]);
91+
}
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy