From f129a5b9b6e69b2f5db7bfba2fc3dd2d17dec15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 5 Jun 2021 15:54:31 +0200 Subject: [PATCH 1/3] Automatically run Loop at end of program (autorun) --- README.md | 35 +++++++++++++++-------- examples/01-timers.php | 2 -- examples/02-periodic.php | 2 -- examples/03-ticks.php | 2 -- examples/04-signals.php | 2 -- examples/11-consume-stdin.php | 2 -- examples/12-generate-yes.php | 2 -- examples/13-http-client-blocking.php | 2 -- examples/14-http-client-async.php | 2 -- examples/21-http-server.php | 2 -- examples/91-benchmark-ticks.php | 2 -- examples/92-benchmark-timers.php | 2 -- examples/93-benchmark-ticks-delay.php | 2 -- examples/94-benchmark-timers-delay.php | 2 -- src/Loop.php | 18 ++++++++++-- tests/BinTest.php | 39 ++++++++++++++++++++++++++ tests/bin/01-ticks-loop-class.php | 13 +++++++++ tests/bin/02-ticks-loop-instance.php | 19 +++++++++++++ tests/bin/03-ticks-loop-stop.php | 23 +++++++++++++++ 19 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 tests/BinTest.php create mode 100644 tests/bin/01-ticks-loop-class.php create mode 100644 tests/bin/02-ticks-loop-instance.php create mode 100644 tests/bin/03-ticks-loop-stop.php diff --git a/README.md b/README.md index ef5c5ce6..e83ded58 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ single [`run()`](#run) call that is controlled by the user. * [Usage](#usage) * [Loop](#loop) * [Loop methods](#loop-methods) + * [Loop autorun](#loop-autorun) * [get()](#get) * [~~Factory~~](#factory) * [~~create()~~](#create) @@ -76,8 +77,6 @@ Loop::addPeriodicTimer(5, function () { $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); - -Loop::run(); ``` See also the [examples](examples). @@ -98,8 +97,6 @@ Loop::addTimer(1.0, function () use ($timer) { Loop::cancelTimer($timer); echo 'Done' . PHP_EOL; }); - -Loop::run(); ``` As an alternative, you can also explicitly create an event loop instance at the @@ -127,12 +124,13 @@ In both cases, the program would perform the exact same steps. 1. The event loop instance is created at the beginning of the program. This is implicitly done the first time you call the [`Loop` class](#loop) or explicitly when using the deprecated [`Factory::create() method`](#create) - (or manually instantiating any of the [loop implementation](#loop-implementations)). + (or manually instantiating any of the [loop implementations](#loop-implementations)). 2. The event loop is used directly or passed as an instance to library and application code. In this example, a periodic timer is registered with the event loop which simply outputs `Tick` every fraction of a second until another timer stops the periodic timer after a second. -3. The event loop is run at the end of the program with a single [`run()`](#run) +3. The event loop is run at the end of the program. This is automatically done + when using [`Loop` class](#loop) or explicitly with a single [`run()`](#run) call at the end of the program. As of `v1.2.0`, we highly recommend using the [`Loop` class](#loop). @@ -176,8 +174,6 @@ Loop::addTimer(1.0, function () use ($timer) { Loop::cancelTimer($timer); echo 'Done' . PHP_EOL; }); - -Loop::run(); ``` On the other hand, if you're familiar with object-oriented programming (OOP) and @@ -208,14 +204,31 @@ class Greeter $greeter = new Greeter(Loop::get()); $greeter->greet('Alice'); $greeter->greet('Bob'); - -Loop::run(); ``` Each static method call will be forwarded as-is to the underlying event loop instance by using the [`Loop::get()`](#get) call internally. See [`LoopInterface`](#loopinterface) for more details about available methods. +#### Loop autorun + +When using the `Loop` class, it will automatically execute the loop at the end of +the program. This means the following example will schedule a timer and will +automatically execute the program until the timer event fires: + +```php +use React\EventLoop\Loop; + +Loop::addTimer(1.0, function () { + echo 'Hello' . PHP_EOL; +}); +``` + +As of `v1.2.0`, we highly recommend using the `Loop` class this way and omitting any +explicit [`run()`](#run) calls. For BC reasons, the explicit [`run()`](#run) +method is still valid and may still be useful in some applications, especially +for a transition period towards the more concise style. + #### get() The `get(): LoopInterface` method can be used to @@ -262,8 +275,6 @@ class Greeter $greeter = new Greeter(Loop::get()); $greeter->greet('Alice'); $greeter->greet('Bob'); - -Loop::run(); ``` See [`LoopInterface`](#loopinterface) for more details about available methods. diff --git a/examples/01-timers.php b/examples/01-timers.php index a7bf3945..5f263b3e 100644 --- a/examples/01-timers.php +++ b/examples/01-timers.php @@ -11,5 +11,3 @@ Loop::addTimer(0.3, function () { echo 'hello '; }); - -Loop::run(); diff --git a/examples/02-periodic.php b/examples/02-periodic.php index 4413870d..68533bd3 100644 --- a/examples/02-periodic.php +++ b/examples/02-periodic.php @@ -12,5 +12,3 @@ Loop::cancelTimer($timer); echo 'Done' . PHP_EOL; }); - -Loop::run(); diff --git a/examples/03-ticks.php b/examples/03-ticks.php index 4b2077da..e32e67af 100644 --- a/examples/03-ticks.php +++ b/examples/03-ticks.php @@ -11,5 +11,3 @@ echo 'c'; }); echo 'a'; - -Loop::run(); diff --git a/examples/04-signals.php b/examples/04-signals.php index ceca3521..e841311b 100644 --- a/examples/04-signals.php +++ b/examples/04-signals.php @@ -15,5 +15,3 @@ }); echo 'Listening for SIGINT. Use "kill -SIGINT ' . getmypid() . '" or CTRL+C' . PHP_EOL; - -Loop::run(); diff --git a/examples/11-consume-stdin.php b/examples/11-consume-stdin.php index f567d84a..dfcb220d 100644 --- a/examples/11-consume-stdin.php +++ b/examples/11-consume-stdin.php @@ -24,5 +24,3 @@ echo strlen($chunk) . ' bytes' . PHP_EOL; }); - -Loop::run(); diff --git a/examples/12-generate-yes.php b/examples/12-generate-yes.php index 4424b8ec..a57e8d6e 100644 --- a/examples/12-generate-yes.php +++ b/examples/12-generate-yes.php @@ -37,5 +37,3 @@ $data = substr($data, $r) . substr($data, 0, $r); } }); - -Loop::run(); diff --git a/examples/13-http-client-blocking.php b/examples/13-http-client-blocking.php index efd8cc86..f0562c90 100644 --- a/examples/13-http-client-blocking.php +++ b/examples/13-http-client-blocking.php @@ -29,5 +29,3 @@ echo $chunk; }); - -Loop::run(); diff --git a/examples/14-http-client-async.php b/examples/14-http-client-async.php index ceed3ec7..074a0eac 100644 --- a/examples/14-http-client-async.php +++ b/examples/14-http-client-async.php @@ -58,5 +58,3 @@ echo $chunk; }); }); - -Loop::run(); diff --git a/examples/21-http-server.php b/examples/21-http-server.php index e000eb51..61529240 100644 --- a/examples/21-http-server.php +++ b/examples/21-http-server.php @@ -32,5 +32,3 @@ $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); - -Loop::run(); diff --git a/examples/91-benchmark-ticks.php b/examples/91-benchmark-ticks.php index 452abbac..e3dc2b1c 100644 --- a/examples/91-benchmark-ticks.php +++ b/examples/91-benchmark-ticks.php @@ -9,5 +9,3 @@ for ($i = 0; $i < $n; ++$i) { Loop::futureTick(function () { }); } - -Loop::run(); diff --git a/examples/92-benchmark-timers.php b/examples/92-benchmark-timers.php index da381f16..dd42ec77 100644 --- a/examples/92-benchmark-timers.php +++ b/examples/92-benchmark-timers.php @@ -9,5 +9,3 @@ for ($i = 0; $i < $n; ++$i) { Loop::addTimer(0, function () { }); } - -Loop::run(); diff --git a/examples/93-benchmark-ticks-delay.php b/examples/93-benchmark-ticks-delay.php index ac5094f3..1976124f 100644 --- a/examples/93-benchmark-ticks-delay.php +++ b/examples/93-benchmark-ticks-delay.php @@ -16,5 +16,3 @@ }; $tick(); - -Loop::run(); diff --git a/examples/94-benchmark-timers-delay.php b/examples/94-benchmark-timers-delay.php index eb4fc5cb..dfe6c8c0 100644 --- a/examples/94-benchmark-timers-delay.php +++ b/examples/94-benchmark-timers-delay.php @@ -16,5 +16,3 @@ }; $tick(); - -Loop::run(); diff --git a/src/Loop.php b/src/Loop.php index fed27cba..8cc9dd8f 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -12,7 +12,6 @@ final class Loop */ private static $instance; - /** * Returns the event loop. * When no loop is set it will it will call the factory to create one. @@ -31,7 +30,22 @@ public static function get() return self::$instance; } - self::$instance = Factory::create(); + self::$instance = $loop = Factory::create(); + + // Automatically run loop at end of program, unless already started explicitly. + // This is tested using child processes, so coverage is actually 100%, see BinTest. + // @codeCoverageIgnoreStart + $hasRun = false; + $loop->futureTick(function () use (&$hasRun) { + $hasRun = true; + }); + + register_shutdown_function(function () use ($loop, &$hasRun) { + if (!$hasRun) { + $loop->run(); + } + }); + // @codeCoverageIgnoreEnd return self::$instance; } diff --git a/tests/BinTest.php b/tests/BinTest.php new file mode 100644 index 00000000..99c05d91 --- /dev/null +++ b/tests/BinTest.php @@ -0,0 +1,39 @@ +markTestSkipped('Tests not supported on legacy PHP 5.3 or HHVM'); + } + + chdir(__DIR__ . '/bin/'); + } + + public function testExecuteExampleWithoutLoopRunRunsLoopAndExecutesTicks() + { + $output = exec(escapeshellarg(PHP_BINARY) . ' 01-ticks-loop-class.php'); + + $this->assertEquals('abc', $output); + } + + public function testExecuteExampleWithExplicitLoopRunRunsLoopAndExecutesTicks() + { + $output = exec(escapeshellarg(PHP_BINARY) . ' 02-ticks-loop-instance.php'); + + $this->assertEquals('abc', $output); + } + + public function testExecuteExampleWithExplicitLoopRunAndStopRunsLoopAndExecutesTicksUntilStopped() + { + $output = exec(escapeshellarg(PHP_BINARY) . ' 03-ticks-loop-stop.php'); + + $this->assertEquals('abc', $output); + } +} diff --git a/tests/bin/01-ticks-loop-class.php b/tests/bin/01-ticks-loop-class.php new file mode 100644 index 00000000..f4fcedf1 --- /dev/null +++ b/tests/bin/01-ticks-loop-class.php @@ -0,0 +1,13 @@ +futureTick(function () { + echo 'b'; +}); + +$loop->futureTick(function () { + echo 'c'; +}); + +echo 'a'; + +$loop->run(); diff --git a/tests/bin/03-ticks-loop-stop.php b/tests/bin/03-ticks-loop-stop.php new file mode 100644 index 00000000..d8b65946 --- /dev/null +++ b/tests/bin/03-ticks-loop-stop.php @@ -0,0 +1,23 @@ +futureTick(function () use ($loop) { + echo 'b'; + + $loop->stop(); + + $loop->futureTick(function () { + echo 'never'; + }); +}); + +echo 'a'; + +$loop->run(); + +echo 'c'; From f0853b115e9f1b922168e6a824ee2ebea7267468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 27 Jun 2021 14:34:39 +0200 Subject: [PATCH 2/3] Don't run loop automatically when an uncaught exceptions occurs --- src/Loop.php | 6 ++++++ tests/BinTest.php | 18 ++++++++++++++++++ tests/bin/11-uncaught.php | 11 +++++++++++ tests/bin/12-undefined.php | 11 +++++++++++ 4 files changed, 46 insertions(+) create mode 100644 tests/bin/11-uncaught.php create mode 100644 tests/bin/12-undefined.php diff --git a/src/Loop.php b/src/Loop.php index 8cc9dd8f..a247579e 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -41,6 +41,12 @@ public static function get() }); register_shutdown_function(function () use ($loop, &$hasRun) { + // Don't run if we're coming from a fatal error (uncaught exception). + $error = error_get_last(); + if ((isset($error['type']) ? $error['type'] : 0) & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR)) { + return; + } + if (!$hasRun) { $loop->run(); } diff --git a/tests/BinTest.php b/tests/BinTest.php index 99c05d91..55b3aaca 100644 --- a/tests/BinTest.php +++ b/tests/BinTest.php @@ -36,4 +36,22 @@ public function testExecuteExampleWithExplicitLoopRunAndStopRunsLoopAndExecutesT $this->assertEquals('abc', $output); } + + public function testExecuteExampleWithUncaughtExceptionShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 11-uncaught.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } + + public function testExecuteExampleWithUndefinedVariableShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 12-undefined.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } } diff --git a/tests/bin/11-uncaught.php b/tests/bin/11-uncaught.php new file mode 100644 index 00000000..0655698b --- /dev/null +++ b/tests/bin/11-uncaught.php @@ -0,0 +1,11 @@ +addTimer(10.0, function () { + echo 'never'; +}); + +$undefined->foo('bar'); From 9712eea0263cd0636c801b2ac71b8b8e3987cc47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 30 Jun 2021 14:59:29 +0200 Subject: [PATCH 3/3] Don't run loop automatically when explicitly calling stop() --- README.md | 19 +++++++++++++++++++ src/Loop.php | 11 ++++++++--- tests/BinTest.php | 18 ++++++++++++++++++ tests/bin/21-stop.php | 11 +++++++++++ tests/bin/22-stop-uncaught.php | 16 ++++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 tests/bin/21-stop.php create mode 100644 tests/bin/22-stop-uncaught.php diff --git a/README.md b/README.md index e83ded58..6b345265 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,25 @@ explicit [`run()`](#run) calls. For BC reasons, the explicit [`run()`](#run) method is still valid and may still be useful in some applications, especially for a transition period towards the more concise style. +If you don't want the `Loop` to run automatically, you can either explicitly +[`run()`](#run) or [`stop()`](#stop) it. This can be useful if you're using +a global exception handler like this: + +```php +use React\EventLoop\Loop; + +Loop::addTimer(10.0, function () { + echo 'Never happens'; +}); + +set_exception_handler(function (Throwable $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + Loop::stop(); +}); + +throw new RuntimeException('Demo'); +``` + #### get() The `get(): LoopInterface` method can be used to diff --git a/src/Loop.php b/src/Loop.php index a247579e..7f1d962c 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -12,6 +12,9 @@ final class Loop */ private static $instance; + /** @var bool */ + private static $stopped = false; + /** * Returns the event loop. * When no loop is set it will it will call the factory to create one. @@ -32,7 +35,7 @@ public static function get() self::$instance = $loop = Factory::create(); - // Automatically run loop at end of program, unless already started explicitly. + // Automatically run loop at end of program, unless already started or stopped explicitly. // This is tested using child processes, so coverage is actually 100%, see BinTest. // @codeCoverageIgnoreStart $hasRun = false; @@ -40,14 +43,15 @@ public static function get() $hasRun = true; }); - register_shutdown_function(function () use ($loop, &$hasRun) { + $stopped =& self::$stopped; + register_shutdown_function(function () use ($loop, &$hasRun, &$stopped) { // Don't run if we're coming from a fatal error (uncaught exception). $error = error_get_last(); if ((isset($error['type']) ? $error['type'] : 0) & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR)) { return; } - if (!$hasRun) { + if (!$hasRun && !$stopped) { $loop->run(); } }); @@ -215,6 +219,7 @@ public static function run() */ public static function stop() { + self::$stopped = true; self::get()->stop(); } } diff --git a/tests/BinTest.php b/tests/BinTest.php index 55b3aaca..6f8231b8 100644 --- a/tests/BinTest.php +++ b/tests/BinTest.php @@ -54,4 +54,22 @@ public function testExecuteExampleWithUndefinedVariableShouldNotRunLoop() $this->assertLessThan(1.0, $time); } + + public function testExecuteExampleWithExplicitStopShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 21-stop.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } + + public function testExecuteExampleWithExplicitStopInExceptionHandlerShouldNotRunLoop() + { + $time = microtime(true); + exec(escapeshellarg(PHP_BINARY) . ' 22-uncaught-stop.php 2>/dev/null'); + $time = microtime(true) - $time; + + $this->assertLessThan(1.0, $time); + } } diff --git a/tests/bin/21-stop.php b/tests/bin/21-stop.php new file mode 100644 index 00000000..038d9223 --- /dev/null +++ b/tests/bin/21-stop.php @@ -0,0 +1,11 @@ +