From 27f0027f2737d9fa11c0d304de93a154dd16cff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 10 Feb 2022 16:29:45 +0100 Subject: [PATCH 1/4] Improve `await()` in `async()` to avoid unneeded `futureTick()` calls --- src/SimpleFiber.php | 4 ++-- tests/AsyncTest.php | 44 ++++++++++++++++++++++++++++++++++++++++++++ tests/AwaitTest.php | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/SimpleFiber.php b/src/SimpleFiber.php index f45e628..e7b866a 100644 --- a/src/SimpleFiber.php +++ b/src/SimpleFiber.php @@ -24,7 +24,7 @@ public function resume(mixed $value): void return; } - Loop::futureTick(fn() => $this->fiber->resume($value)); + $this->fiber->resume($value); } public function throw(\Throwable $throwable): void @@ -34,7 +34,7 @@ public function throw(\Throwable $throwable): void return; } - Loop::futureTick(fn() => $this->fiber->throw($throwable)); + $this->fiber->throw($throwable); } public function suspend(): mixed diff --git a/tests/AsyncTest.php b/tests/AsyncTest.php index dccbe54..82f98f0 100644 --- a/tests/AsyncTest.php +++ b/tests/AsyncTest.php @@ -4,6 +4,7 @@ use React; use React\EventLoop\Loop; +use React\Promise\Deferred; use React\Promise\Promise; use function React\Async\async; use function React\Async\await; @@ -84,6 +85,49 @@ public function testAsyncReturnsPendingPromiseWhenCallbackReturnsPendingPromise( $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } + public function testAsyncWithAwaitReturnsReturnsPromiseFulfilledWithValueImmediatelyWhenPromiseIsFulfilled() + { + $deferred = new Deferred(); + + $promise = async(function () use ($deferred) { + return await($deferred->promise()); + })(); + + $return = null; + $promise->then(function ($value) use (&$return) { + $return = $value; + }); + + $this->assertNull($return); + + $deferred->resolve(42); + + $this->assertEquals(42, $return); + } + + public function testAsyncWithAwaitReturnsPromiseRejectedWithExceptionImmediatelyWhenPromiseIsRejected() + { + $deferred = new Deferred(); + + $promise = async(function () use ($deferred) { + return await($deferred->promise()); + })(); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + $this->assertNull($exception); + + $deferred->reject(new \RuntimeException('Test', 42)); + + $this->assertInstanceof(\RuntimeException::class, $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Test', $exception->getMessage()); + $this->assertEquals(42, $exception->getCode()); + } + public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingPromise() { $promise = async(function () { diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 2bf1314..6ef938f 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -22,6 +22,27 @@ public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(calla $await($promise); } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsExceptionWithoutRunningLoop(callable $await) + { + $now = true; + Loop::futureTick(function () use (&$now) { + $now = false; + }); + + $promise = new Promise(function () { + throw new \Exception('test'); + }); + + try { + $await($promise); + } catch (\Exception $e) { + $this->assertTrue($now); + } + } + /** * @dataProvider provideAwaiters */ @@ -91,6 +112,24 @@ public function testAwaitReturnsValueWhenPromiseIsFullfilled(callable $await) $this->assertEquals(42, $await($promise)); } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitReturnsValueImmediatelyWithoutRunningLoop(callable $await) + { + $now = true; + Loop::futureTick(function () use (&$now) { + $now = false; + }); + + $promise = new Promise(function ($resolve) { + $resolve(42); + }); + + $this->assertEquals(42, $await($promise)); + $this->assertTrue($now); + } + /** * @dataProvider provideAwaiters */ From 8f01f4b777a39bd02a021a3df3eeb2604a8e889d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 16 Feb 2022 17:52:43 +0100 Subject: [PATCH 2/4] Improve `await()` in main to avoid unneeded `futureTick()` calls --- src/SimpleFiber.php | 12 ++++++++++-- tests/AsyncTest.php | 16 ++++++++++++++++ tests/AwaitTest.php | 46 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/SimpleFiber.php b/src/SimpleFiber.php index e7b866a..0aa272a 100644 --- a/src/SimpleFiber.php +++ b/src/SimpleFiber.php @@ -20,7 +20,11 @@ public function __construct() public function resume(mixed $value): void { if ($this->fiber === null) { - Loop::futureTick(static fn() => \Fiber::suspend(static fn() => $value)); + if (\Fiber::getCurrent() !== self::$scheduler) { + Loop::futureTick(static fn() => \Fiber::suspend(static fn() => $value)); + } else { + \Fiber::suspend(static fn() => $value); + } return; } @@ -30,7 +34,11 @@ public function resume(mixed $value): void public function throw(\Throwable $throwable): void { if ($this->fiber === null) { - Loop::futureTick(static fn() => \Fiber::suspend(static fn() => throw $throwable)); + if (\Fiber::getCurrent() !== self::$scheduler) { + Loop::futureTick(static fn() => \Fiber::suspend(static fn() => throw $throwable)); + } else { + \Fiber::suspend(static fn() => throw $throwable); + } return; } diff --git a/tests/AsyncTest.php b/tests/AsyncTest.php index 82f98f0..a4287fd 100644 --- a/tests/AsyncTest.php +++ b/tests/AsyncTest.php @@ -143,6 +143,22 @@ public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsA $this->assertEquals(42, $value); } + public function testAsyncReturnsPromiseThatRejectsWithExceptionWhenCallbackThrowsAfterAwaitingPromise() + { + $promise = async(function () { + $promise = new Promise(function ($_, $reject) { + Loop::addTimer(0.001, fn () => $reject(new \RuntimeException('Foo', 42))); + }); + + return await($promise); + })(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Foo'); + $this->expectExceptionCode(42); + await($promise); + } + public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingTwoConcurrentPromises() { $promise1 = async(function () { diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 6ef938f..c055332 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -4,6 +4,7 @@ use React; use React\EventLoop\Loop; +use React\Promise\Deferred; use React\Promise\Promise; class AwaitTest extends TestCase @@ -43,6 +44,30 @@ public function testAwaitThrowsExceptionWithoutRunningLoop(callable $await) } } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsExceptionImmediatelyWhenPromiseIsRejected(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->reject(new \RuntimeException())); + + try { + $await($deferred->promise()); + } catch (\RuntimeException $e) { + $this->assertEquals(1, $ticks); + } + } + /** * @dataProvider provideAwaiters */ @@ -130,6 +155,27 @@ public function testAwaitReturnsValueImmediatelyWithoutRunningLoop(callable $awa $this->assertTrue($now); } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->resolve(42)); + + $this->assertEquals(42, $await($deferred->promise())); + $this->assertEquals(1, $ticks); + } + /** * @dataProvider provideAwaiters */ From c5d53ee64a3ca8b3f1128ba4ada578e4cef17ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 17 Feb 2022 08:53:23 +0100 Subject: [PATCH 3/4] Improve `await()` for `asyc()` to avoid unneeded `futureTick()` calls --- src/SimpleFiber.php | 25 +++++++++++++++++---- tests/AwaitTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/SimpleFiber.php b/src/SimpleFiber.php index 0aa272a..ed20e2a 100644 --- a/src/SimpleFiber.php +++ b/src/SimpleFiber.php @@ -10,6 +10,7 @@ final class SimpleFiber implements FiberInterface { private static ?\Fiber $scheduler = null; + private static ?\Closure $suspend = null; private ?\Fiber $fiber = null; public function __construct() @@ -20,29 +21,45 @@ public function __construct() public function resume(mixed $value): void { if ($this->fiber === null) { + $suspend = static fn() => $value; if (\Fiber::getCurrent() !== self::$scheduler) { - Loop::futureTick(static fn() => \Fiber::suspend(static fn() => $value)); + self::$suspend = $suspend; } else { - \Fiber::suspend(static fn() => $value); + \Fiber::suspend($suspend); } return; } $this->fiber->resume($value); + + if (self::$suspend) { + $suspend = self::$suspend; + self::$suspend = null; + + \Fiber::suspend($suspend); + } } public function throw(\Throwable $throwable): void { if ($this->fiber === null) { + $suspend = static fn() => throw $throwable; if (\Fiber::getCurrent() !== self::$scheduler) { - Loop::futureTick(static fn() => \Fiber::suspend(static fn() => throw $throwable)); + self::$suspend = $suspend; } else { - \Fiber::suspend(static fn() => throw $throwable); + \Fiber::suspend($suspend); } return; } $this->fiber->throw($throwable); + + if (self::$suspend) { + $suspend = self::$suspend; + self::$suspend = null; + + \Fiber::suspend($suspend); + } } public function suspend(): mixed diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index c055332..2dd8159 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -6,6 +6,7 @@ use React\EventLoop\Loop; use React\Promise\Deferred; use React\Promise\Promise; +use function React\Async\async; class AwaitTest extends TestCase { @@ -68,6 +69,34 @@ public function testAwaitThrowsExceptionImmediatelyWhenPromiseIsRejected(callabl } } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitAsyncThrowsExceptionImmediatelyWhenPromiseIsRejected(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->reject(new \RuntimeException())); + + $promise = async(function () use ($deferred, $await) { + return $await($deferred->promise()); + })(); + + try { + $await($promise); + } catch (\RuntimeException $e) { + $this->assertEquals(1, $ticks); + } + } + /** * @dataProvider provideAwaiters */ @@ -176,6 +205,31 @@ public function testAwaitReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $this->assertEquals(1, $ticks); } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitAsyncReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->resolve(42)); + + $promise = async(function () use ($deferred, $await) { + return $await($deferred->promise()); + })(); + + $this->assertEquals(42, $await($promise)); + $this->assertEquals(1, $ticks); + } + /** * @dataProvider provideAwaiters */ From 4d8331fbfabc0e37cdfae898684da497fbf89ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 17 Feb 2022 10:45:00 +0100 Subject: [PATCH 4/4] Refactor `SimpleFiber` to simplify async code flow --- src/SimpleFiber.php | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/SimpleFiber.php b/src/SimpleFiber.php index ed20e2a..acf3fad 100644 --- a/src/SimpleFiber.php +++ b/src/SimpleFiber.php @@ -20,19 +20,13 @@ public function __construct() public function resume(mixed $value): void { - if ($this->fiber === null) { - $suspend = static fn() => $value; - if (\Fiber::getCurrent() !== self::$scheduler) { - self::$suspend = $suspend; - } else { - \Fiber::suspend($suspend); - } - return; + if ($this->fiber !== null) { + $this->fiber->resume($value); + } else { + self::$suspend = static fn() => $value; } - $this->fiber->resume($value); - - if (self::$suspend) { + if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { $suspend = self::$suspend; self::$suspend = null; @@ -42,19 +36,13 @@ public function resume(mixed $value): void public function throw(\Throwable $throwable): void { - if ($this->fiber === null) { - $suspend = static fn() => throw $throwable; - if (\Fiber::getCurrent() !== self::$scheduler) { - self::$suspend = $suspend; - } else { - \Fiber::suspend($suspend); - } - return; + if ($this->fiber !== null) { + $this->fiber->throw($throwable); + } else { + self::$suspend = static fn() => throw $throwable; } - $this->fiber->throw($throwable); - - if (self::$suspend) { + if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { $suspend = self::$suspend; self::$suspend = null;