Skip to content

[RFC] Global Loop #77

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

Closed
wants to merge 4 commits into from
Closed
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
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,25 @@ All of the loops support these features:

Here is an async HTTP server built with just the event loop.
```php
$loop = React\EventLoop\Factory::create();

$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, 0);
$loop->addReadStream($server, function ($server) use ($loop) {
React\EventLoop\addReadStream($server, function ($server) {
$conn = stream_socket_accept($server);
$data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n";
$loop->addWriteStream($conn, function ($conn) use (&$data, $loop) {
React\EventLoop\addWriteStream($conn, function ($conn) use (&$data) {
$written = fwrite($conn, $data);
if ($written === strlen($data)) {
fclose($conn);
$loop->removeStream($conn);
React\EventLoop\removeStream($conn);
} else {
$data = substr($data, $written);
}
});
});

$loop->addPeriodicTimer(5, function () {
React\EventLoop\addPeriodicTimer(5, function () {
$memory = memory_get_usage() / 1024;
$formatted = number_format($memory, 3).'K';
echo "Current memory usage: {$formatted}\n";
});

$loop->run();
```
**Note:** The factory is just for convenience. It tries to pick the best
available implementation. Libraries `SHOULD` allow the user to inject an
instance of the loop. They `MAY` use the factory when the user did not supply
a loop.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"autoload": {
"psr-4": {
"React\\EventLoop\\": "src"
}
},
"files": ["src/functions.php"]
Copy link
Member

Choose a reason for hiding this comment

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

Personally I'm fan of using an intermediary functions_include.php like this, this avoids weird edge cases when using threads:

Copy link
Member Author

Choose a reason for hiding this comment

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

Of course, thanks for the reminder!

Copy link
Contributor

Choose a reason for hiding this comment

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

@WyriHaximus Which edge cases exist there? Could you elaborate?

Copy link
Member Author

Choose a reason for hiding this comment

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

See reactphp/promise#23 and reactphp/promise#25. Might be fixed in the meantime by composer/composer#4186.

}
}
72 changes: 72 additions & 0 deletions src/GlobalLoop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace React\EventLoop;

final class GlobalLoop
{
/**
* @internal
*
* @var LoopInterface
*/
public static $loop;
Copy link
Member

Choose a reason for hiding this comment

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

Why not always force GlobalLoop::get? Making this public could potentially mean someone swaps it out and breaks everything.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is currently public for 2 reasons:

  1. It allows to save a function call in the function API, see eg. https://github.com/reactphp/event-loop/pull/77/files#diff-84b928a9905378810b602c8a740b4cbaR16
  2. It allows to swap out the loop, see eg. https://github.com/reactphp/event-loop/pull/77/files#diff-6872a075db159cbbcfc073b693c7d460R18

Note, that the property is marked @internal, so it should be pretty clear the messing around with it isn't a good idea ;)

Again, this is just a POC, so we might decide to make it private, always use GlobalLoop::get() and add a corresponding GlobalLoop::set() for swapping out the loop, eg. in tests.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense, lets see what @clue, @cboden, and @nrk have to say and get the ball rolling if they like it 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

@jsor @WyriHaximus This is one reason why https://github.com/async-interop/event-loop uses a static class instead of functions and has the clear Loop::execute scoping. The current loop can be a private property and swapping out the loop is very easy and not something you have to worry about.


private static $factory = ['React\EventLoop\Factory', 'create'];

private static $disableRunOnShutdown = false;

public static function setFactory(callable $factory)
{
if (self::$loop) {
throw new \LogicException(
'Setting a factory after the global loop has been created is not allowed.'
);
Copy link
Contributor

Choose a reason for hiding this comment

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

How can it be reset to work in tests?

}

self::$factory = $factory;
}

public function disableRunOnShutdown()
{
self::$disableRunOnShutdown = true;
}

/**
* @return LoopInterface
*/
public static function get()
{
if (self::$loop) {
return self::$loop;
}

register_shutdown_function(function () {
if (self::$disableRunOnShutdown || !self::$loop) {
return;
}

self::$loop->run();
});

return self::$loop = self::create();
}

/**
* @return LoopInterface
*/
public static function create()
{
$loop = call_user_func(self::$factory);

if (!$loop instanceof LoopInterface) {
throw new \LogicException(
sprintf(
'The GlobalLoop factory must return an instance of LoopInterface but returned %s.',
is_object($loop) ? get_class($loop) : gettype($loop)
)
);
}

return $loop;
}
}
123 changes: 123 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace React\EventLoop;

use React\EventLoop\Timer\TimerInterface;

/**
* Register a listener to the global event loop to be notified when a stream is
* ready to read.
*
* @param resource $stream The PHP stream resource to check.
* @param callable $listener Invoked when the stream is ready.
*/
function addReadStream($stream, callable $listener)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

$loop->addReadStream($stream, $listener);
}

/**
* Register a listener to the global event loop to be notified when a stream is
* ready to write.
*
* @param resource $stream The PHP stream resource to check.
* @param callable $listener Invoked when the stream is ready.
*/
function addWriteStream($stream, callable $listener)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

$loop->addWriteStream($stream, $listener);
}

/**
* Remove the read event listener from the global event loop for the given
* stream.
*
* @param resource $stream The PHP stream resource.
*/
function removeReadStream($stream)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

$loop->removeReadStream($stream);
}

/**
* Remove the write event listener from the global event loop for the given
* stream.
*
* @param resource $stream The PHP stream resource.
*/
function removeWriteStream($stream)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

$loop->removeWriteStream($stream);
}

/**
* Remove all listeners from the global event loop for the given stream.
*
* @param resource $stream The PHP stream resource.
*/
function removeStream($stream)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

$loop->removeStream($stream);
}

/**
* Enqueue a callback to the global event loop to be invoked once after the
* given interval.
*
* The execution order of timers scheduled to execute at the same time is
* not guaranteed.
*
* @param int|float $interval The number of seconds to wait before execution.
* @param callable $callback The callback to invoke.
*
* @return TimerInterface
*/
function addTimer($interval, callable $callback)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

return $loop->addTimer($interval, $callback);
}

/**
* Enqueue a callback to the global event loop to be invoked repeatedly after
* the given interval.
*
* The execution order of timers scheduled to execute at the same time is
* not guaranteed.
*
* @param int|float $interval The number of seconds to wait before execution.
* @param callable $callback The callback to invoke.
*
* @return TimerInterface
*/
function addPeriodicTimer($interval, callable $callback)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

return $loop->addPeriodicTimer($interval, $callback);
}

/**
* Schedule a callback to be invoked on a future tick of the global event loop.
*
* Callbacks are guaranteed to be executed in the order they are enqueued.
*
* @param callable $listener The callback to invoke.
*/
function futureTick(callable $listener)
{
$loop = GlobalLoop::$loop ?: GlobalLoop::get();

$loop->futureTick($listener);
}
121 changes: 121 additions & 0 deletions tests/FunctionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

namespace React\Tests\EventLoop;

use React\EventLoop;
use React\EventLoop\GlobalLoop;

class FunctionTest extends TestCase
{
private static $state;
private $globalLoop;

public function setUp()
{
$globalLoop = $this->getMockBuilder('React\EventLoop\LoopInterface')
->getMock();

self::$state = GlobalLoop::$loop;
GlobalLoop::$loop = $this->globalLoop = $globalLoop;
}

public function tearDown()
{
$this->globalLoop = null;

GlobalLoop::$loop = self::$state;
}

public function createStream()
{
return fopen('php://temp', 'r+');
}

public function testAddReadStream()
{
$stream = $this->createStream();
$listener = function() {};

$this->globalLoop
->expects($this->once())
->method('addReadStream')
->with($stream, $listener);

EventLoop\addReadStream($stream, $listener);
}

public function testAddWriteStream()
{
$stream = $this->createStream();
$listener = function() {};

$this->globalLoop
->expects($this->once())
->method('addWriteStream')
->with($stream, $listener);

EventLoop\addWriteStream($stream, $listener);
}

public function testRemoveReadStream()
{
$stream = $this->createStream();

$this->globalLoop
->expects($this->once())
->method('removeReadStream')
->with($stream);

EventLoop\removeReadStream($stream);
}

public function testRemoveWriteStream()
{
$stream = $this->createStream();

$this->globalLoop
->expects($this->once())
->method('removeWriteStream')
->with($stream);

EventLoop\removeWriteStream($stream);
}

public function testRemoveStream()
{
$stream = $this->createStream();

$this->globalLoop
->expects($this->once())
->method('removeStream')
->with($stream);

EventLoop\removeStream($stream);
}

public function testAddTimer()
{
$interval = 1;
$listener = function() {};

$this->globalLoop
->expects($this->once())
->method('addTimer')
->with($interval, $listener);

EventLoop\addTimer($interval, $listener);
}

public function testAddPeriodicTimer()
{
$interval = 1;
$listener = function() {};

$this->globalLoop
->expects($this->once())
->method('addPeriodicTimer')
->with($interval, $listener);

EventLoop\addPeriodicTimer($interval, $listener);
}
}
Loading
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