diff --git a/src/StreamSelectLoop.php b/src/StreamSelectLoop.php index 92fc5787..b2aba72b 100644 --- a/src/StreamSelectLoop.php +++ b/src/StreamSelectLoop.php @@ -13,6 +13,8 @@ class StreamSelectLoop implements LoopInterface { const MICROSECONDS_PER_SECOND = 1000000; + const NANOSECONDS_PER_SECOND = 1000000000; + const NANOSECONDS_PER_MICROSECOND = 1000; private $futureTickQueue; private $timers; @@ -152,20 +154,9 @@ public function run() // Future-tick queue has pending callbacks ... if (!$this->running || !$this->futureTickQueue->isEmpty()) { $timeout = 0; - // There is a pending timer, only block until it is due ... } elseif ($scheduledAt = $this->timers->getFirst()) { - $timeout = $scheduledAt - $this->timers->getTime(); - if ($timeout < 0) { - $timeout = 0; - } else { - /* - * round() needed to correct float error: - * https://github.com/reactphp/event-loop/issues/48 - */ - $timeout = round($timeout * self::MICROSECONDS_PER_SECOND); - } - + $timeout = max($scheduledAt - $this->timers->getTime(), 0); // The only possible event is stream activity, so wait forever ... } elseif ($this->readStreams || $this->writeStreams) { $timeout = null; @@ -189,6 +180,8 @@ public function stop() /** * Wait/check for stream activity, or until the next timer is due. + * + * @param float $timeout */ private function waitForStreamActivity($timeout) { @@ -219,28 +212,118 @@ private function waitForStreamActivity($timeout) } } + /** + * Returns integer amount of seconds in $time. + * + * @param float $time – time in seconds + * + * @return int + */ + private static function getSeconds($time) + { + /* + * intval(floor($time)) will produce int overflow + * if $time is (float)PHP_INT_MAX: + * (float)PHP_INT_MAX == PHP_INT_MAX //true + * (int)(float)PHP_INT_MAX == PHP_INT_MAX //false + * (int)(float)PHP_INT_MAX == PHP_INT_MIN //true + * ----------------------------------------------- + * Loose comparision is intentional, cause $time + * is float and + * (float)PHP_INT_MAX !== PHP_INT_MAX + */ + if ($time == PHP_INT_MAX) { + return PHP_INT_MAX; + } + + return intval(floor($time)); + } + + /** + * Returns integer amount of microseconds in $time. + * + * @param float $time – time in seconds + * + * @return int + */ + private static function getMicroseconds($time) + { + $fractional = fmod($time, 1); + $microseconds = round($fractional * self::MICROSECONDS_PER_SECOND); + + return intval($microseconds); + } + + /** + * Returns integer amount of nanoseconds in $time. + * The precision is 1 microsecond. + * + * @param float $time – time in seconds + * + * @return int + */ + private static function getNanoseconds($time) + { + return intval(self::getMicroseconds($time) * self::NANOSECONDS_PER_MICROSECOND); + } + /** * Emulate a stream_select() implementation that does not break when passed * empty stream arrays. * * @param array &$read An array of read streams to select upon. * @param array &$write An array of write streams to select upon. - * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + * @param float|null $timeout Activity timeout in seconds, or null to wait forever. * * @return integer|false The total number of streams that are ready for read/write. * Can return false if stream_select() is interrupted by a signal. */ protected function streamSelect(array &$read, array &$write, $timeout) { + $seconds = $timeout === null ? null : self::getSeconds($timeout); + if ($read || $write) { - $except = null; + $except = []; + $microseconds = $timeout === null ? 0 : self::getMicroseconds($timeout); - // suppress warnings that occur, when stream_select is interrupted by a signal - return @stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + return $this->doSelectStream($read, $write, $except, $seconds, $microseconds); } - $timeout && usleep($timeout); + if ($timeout !== null) { + $nanoseconds = self::getNanoseconds($timeout); + $this->sleep($seconds, $nanoseconds); + } return 0; } + + /** + * Proxy for built-in stream_select method. + * + * @param array $read + * @param array $write + * @param array $except + * @param int|null $seconds + * @param int $microseconds + * + * @return int + */ + protected function doSelectStream(array &$read, array &$write, array &$except, $seconds, $microseconds) + { + // suppress warnings that occur, when stream_select is interrupted by a signal + return @stream_select($read, $write, $except, $seconds, $microseconds); + } + + /** + * Sleeps for $seconds and $nanoseconds. + * + * @param int $seconds + * @param int $nanoseconds + */ + protected function sleep($seconds, $nanoseconds = 0) + { + if ($seconds > 0 || $nanoseconds > 0) { + time_nanosleep($seconds, $nanoseconds); + } + } } diff --git a/tests/StreamSelectLoopTest.php b/tests/StreamSelectLoopTest.php index d2e3e078..387bcd94 100644 --- a/tests/StreamSelectLoopTest.php +++ b/tests/StreamSelectLoopTest.php @@ -156,15 +156,15 @@ protected function forkSendSignal($signal) public function testSmallTimerInterval() { /** @var StreamSelectLoop|\PHPUnit_Framework_MockObject_MockObject $loop */ - $loop = $this->getMock('React\EventLoop\StreamSelectLoop', ['streamSelect']); + $loop = $this->getMock('React\EventLoop\StreamSelectLoop', ['sleep']); $loop ->expects($this->at(0)) - ->method('streamSelect') - ->with([], [], 1); + ->method('sleep') + ->with(0, intval(Timer::MIN_INTERVAL * StreamSelectLoop::NANOSECONDS_PER_SECOND)); $loop ->expects($this->at(1)) - ->method('streamSelect') - ->with([], [], 0); + ->method('sleep') + ->with(0, 0); $callsCount = 0; $loop->addPeriodicTimer(Timer::MIN_INTERVAL, function() use (&$loop, &$callsCount) { @@ -176,4 +176,23 @@ public function testSmallTimerInterval() $loop->run(); } + + /** + * https://github.com/reactphp/event-loop/issues/19 + * + * Tests that timer with PHP_INT_MAX seconds interval will work. + */ + public function testIntOverflowTimerInterval() + { + /** @var StreamSelectLoop|\PHPUnit_Framework_MockObject_MockObject $loop */ + $loop = $this->getMock('React\EventLoop\StreamSelectLoop', ['sleep']); + $loop->expects($this->once()) + ->method('sleep') + ->with(PHP_INT_MAX, 0) + ->willReturnCallback(function() use (&$loop) { + $loop->stop(); + }); + $loop->addTimer(PHP_INT_MAX, function(){}); + $loop->run(); + } }