Skip to content

Commit 67ac413

Browse files
authored
Fiberize error handler (#10)
1 parent 70e67dc commit 67ac413

3 files changed

Lines changed: 61 additions & 20 deletions

File tree

src/EventLoop.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,11 @@ public static function run(): void
391391
self::$fiber = self::createFiber();
392392
}
393393

394-
if (self::$fiber->isStarted()) {
395-
self::$fiber->resume();
396-
} else {
397-
self::$fiber->start();
394+
$lambda = self::$fiber->isStarted() ? self::$fiber->resume() : self::$fiber->start();
395+
396+
if ($lambda) {
397+
$lambda();
398+
throw new \Error('Interrupt from event loop must throw an exception');
398399
}
399400
}
400401

src/EventLoop/Internal/AbstractDriver.php

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ abstract class AbstractDriver implements Driver
2525

2626
private \Fiber $callbackFiber;
2727
private \Fiber $queueFiber;
28+
private \Closure $errorCallback;
2829

2930
/** @var Callback[] */
3031
private array $callbacks = [];
@@ -51,15 +52,18 @@ abstract class AbstractDriver implements Driver
5152

5253
private bool $running = false;
5354

55+
private bool $inFiber = false;
56+
5457
private \stdClass $internalSuspensionMarker;
5558

5659
public function __construct()
5760
{
5861
$this->internalSuspensionMarker = new \stdClass();
5962
$this->createCallbackFiber();
6063
$this->createQueueFiber();
64+
$this->createErrorCallback();
6165
/** @psalm-suppress InvalidArgument */
62-
$this->interruptCallback = \Closure::fromCallable([$this, 'interrupt']);
66+
$this->interruptCallback = \Closure::fromCallable([$this, 'setInterrupt']);
6367
}
6468

6569
/**
@@ -84,9 +88,14 @@ public function run(): void
8488
}
8589

8690
$this->running = true;
91+
$this->inFiber = \Fiber::getCurrent() !== null;
8792

8893
try {
8994
while ($this->running) {
95+
if ($this->interrupt) {
96+
$this->invokeInterrupt();
97+
}
98+
9099
$this->invokeMicrotasks();
91100

92101
if ($this->isEmpty()) {
@@ -97,6 +106,7 @@ public function run(): void
97106
}
98107
} finally {
99108
$this->running = false;
109+
$this->inFiber = false;
100110
}
101111
}
102112

@@ -594,10 +604,7 @@ protected function invokeCallback(Callback $callback): void
594604
}
595605

596606
if ($this->interrupt) {
597-
$interrupt = $this->interrupt;
598-
$this->interrupt = null;
599-
600-
\Fiber::suspend($interrupt);
607+
$this->invokeInterrupt();
601608
}
602609

603610
if ($this->microQueue) {
@@ -615,10 +622,12 @@ protected function invokeCallback(Callback $callback): void
615622
protected function error(\Throwable $exception): void
616623
{
617624
if ($this->errorHandler === null) {
618-
throw $exception;
625+
$this->setInterrupt(static fn () => throw $exception);
626+
return;
619627
}
620628

621-
($this->errorHandler)($exception);
629+
$fiber = new \Fiber($this->errorCallback);
630+
$fiber->start($this->errorHandler, $exception);
622631
}
623632

624633
/**
@@ -698,18 +707,31 @@ private function invokeMicrotasks(): void
698707
}
699708

700709
if ($this->interrupt) {
701-
$interrupt = $this->interrupt;
702-
$this->interrupt = null;
703-
704-
\Fiber::suspend($interrupt);
710+
$this->invokeInterrupt();
705711
}
706712
}
707713
}
708714
}
709715

710-
private function interrupt(callable $callback): void
716+
private function setInterrupt(callable $interrupt): void
711717
{
712-
$this->interrupt = $callback;
718+
\assert($this->interrupt === null);
719+
$this->interrupt = $interrupt;
720+
}
721+
722+
private function invokeInterrupt(): void
723+
{
724+
\assert($this->interrupt !== null);
725+
726+
$interrupt = $this->interrupt;
727+
$this->interrupt = null;
728+
729+
if (!$this->inFiber) {
730+
$interrupt();
731+
throw new \Error('Interrupt must throw if not executing in a fiber');
732+
}
733+
734+
\Fiber::suspend($interrupt);
713735
}
714736

715737
private function createCallbackFiber(): void
@@ -749,4 +771,15 @@ private function createQueueFiber(): void
749771

750772
$this->queueFiber->start();
751773
}
774+
775+
private function createErrorCallback(): void
776+
{
777+
$this->errorCallback = function (callable $errorHandler, \Throwable $exception): void {
778+
try {
779+
$errorHandler($exception);
780+
} catch (\Throwable $exception) {
781+
$this->interrupt = static fn () => throw $exception;
782+
}
783+
};
784+
}
752785
}

src/EventLoop/Suspension.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ final class Suspension
2121
private \Fiber $scheduler;
2222
private Driver $driver;
2323
private bool $pending = false;
24+
private ?\FiberError $error = null;
2425
/** @var callable */
2526
private $interrupt;
2627

@@ -45,7 +46,7 @@ public function __construct(Driver $driver, \Fiber $scheduler, callable $interru
4546
public function throw(\Throwable $throwable): void
4647
{
4748
if (!$this->pending) {
48-
throw new \Error('Must call throw() before calling resume()');
49+
throw $this->error ?? new \Error('Must call suspend() before calling throw()');
4950
}
5051

5152
$this->pending = false;
@@ -61,7 +62,7 @@ public function throw(\Throwable $throwable): void
6162
public function resume(mixed $value): void
6263
{
6364
if (!$this->pending) {
64-
throw new \Error('Must call suspend() before calling resume()');
65+
throw $this->error ?? new \Error('Must call suspend() before calling resume()');
6566
}
6667

6768
$this->pending = false;
@@ -88,7 +89,13 @@ public function suspend(): mixed
8889

8990
// Awaiting from within a fiber.
9091
if ($this->fiber) {
91-
return \Fiber::suspend();
92+
try {
93+
return \Fiber::suspend();
94+
} catch (\FiberError $exception) {
95+
$this->pending = false;
96+
$this->error = $exception;
97+
throw $exception;
98+
}
9299
}
93100

94101
// Awaiting from {main}.

0 commit comments

Comments
 (0)