Skip to content

Commit

Permalink
Ignore spurious resumes of old dead {main} suspensions
Browse files Browse the repository at this point in the history
  • Loading branch information
danog committed Nov 16, 2023
1 parent ccf9d00 commit ffb30e2
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 4 deletions.
21 changes: 19 additions & 2 deletions src/EventLoop/Internal/DriverSuspension.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ final class DriverSuspension implements Suspension

private bool $pending = false;

private bool $deadMain = false;

public function __construct(
private readonly \Closure $run,
private readonly \Closure $queue,
Expand All @@ -38,6 +40,11 @@ public function __construct(

public function resume(mixed $value = null): void
{
// Ignore spurious resumes to old dead {main} suspension
if ($this->deadMain) {
return;
}

if (!$this->pending) {
throw $this->error ?? new \Error('Must call suspend() before calling resume()');
}
Expand All @@ -62,6 +69,10 @@ public function resume(mixed $value = null): void

public function suspend(): mixed
{
// Throw exception when trying to use old dead {main} suspension
if ($this->deadMain) {
throw $this->error;
}
if ($this->pending) {
throw new \Error('Must call resume() or throw() before calling suspend() again');
}
Expand Down Expand Up @@ -101,13 +112,14 @@ public function suspend(): mixed

/** @psalm-suppress RedundantCondition $this->pending should be changed when resumed. */
if ($this->pending) {
$this->pending = false;
// This is now a dead {main} suspension.
$this->deadMain = true;

try {
$result && $result(); // Unwrap any uncaught exceptions from the event loop
} catch (\Throwable $throwable) {
$this->error = new \Error(
'Suspension cannot be resumed after an uncaught exception is thrown from the event loop',
'Suspension cannot be suspended after an uncaught exception is thrown from the event loop',
);

throw $throwable;
Expand Down Expand Up @@ -137,6 +149,11 @@ public function suspend(): mixed

public function throw(\Throwable $throwable): void
{
// Ignore spurious resumes to old dead {main} suspension
if ($this->deadMain) {
return;
}

if (!$this->pending) {
throw $this->error ?? new \Error('Must call suspend() before calling throw()');
}
Expand Down
36 changes: 34 additions & 2 deletions test/EventLoopTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,14 @@ public function testSuspensionThrowingErrorViaInterrupt(): void
self::assertSame($error, $t->getPrevious());
}

$suspension->resume(); // Calling resume on the same suspension should not throw an Error.
$suspension->throw(new \RuntimeException()); // Calling throw on the same suspension should not throw an Error.

try {
$suspension->resume(); // Calling resume on the same suspension should throw an Error.
$suspension->suspend(); // Calling suspend on the same suspension should throw an Error.
self::fail("Error was not thrown");
} catch (\Error $e) {
self::assertStringContainsString('resumed after an uncaught exception', $e->getMessage());
self::assertStringContainsString('suspended after an uncaught exception', $e->getMessage());
}

// Creating a new Suspension and re-entering the event loop (e.g. in a shutdown function) should work.
Expand All @@ -312,6 +315,35 @@ public function testSuspensionThrowingErrorViaInterrupt(): void
$suspension->suspend();
}

public function testSuspensionThrowingErrorViaInterrupt2(): void
{
$suspension = EventLoop::getSuspension();
$error = new \Error("Test error");
EventLoop::queue(static fn () => throw $error);
EventLoop::queue($suspension->resume(...), 123);
try {
$suspension->suspend();
self::fail("Error was not thrown");
} catch (UncaughtThrowable $t) {
self::assertSame($error, $t->getPrevious());
}

$suspension->resume(); // Calling resume on the same suspension should not throw an Error.
$suspension->throw(new \RuntimeException()); // Calling throw on the same suspension should not throw an Error.

try {
$suspension->suspend(); // Calling suspend on the same suspension should throw an Error.
self::fail("Error was not thrown");
} catch (\Error $e) {
self::assertStringContainsString('suspended after an uncaught exception', $e->getMessage());
}

// Creating a new Suspension and re-entering the event loop (e.g. in a shutdown function) should work.
$suspension = EventLoop::getSuspension();
EventLoop::queue($suspension->resume(...), 321);
$this->assertEquals(321, $suspension->suspend());
}

public function testFiberDestroyedWhileSuspended(): void
{
$outer = new class (new class ($this) {
Expand Down

0 comments on commit ffb30e2

Please sign in to comment.