diff --git a/src/EventLoop/Internal/AbstractDriver.php b/src/EventLoop/Internal/AbstractDriver.php index fc2e732..58e7bcc 100644 --- a/src/EventLoop/Internal/AbstractDriver.php +++ b/src/EventLoop/Internal/AbstractDriver.php @@ -48,6 +48,8 @@ abstract class AbstractDriver implements Driver private readonly \Closure $interruptCallback; private readonly \Closure $queueCallback; + + /** @var \Closure():(null|\Closure(): mixed) */ private readonly \Closure $runCallback; private readonly \stdClass $internalSuspensionMarker; @@ -87,12 +89,19 @@ public function __construct() /** @psalm-suppress InvalidArgument */ $this->interruptCallback = $this->setInterrupt(...); $this->queueCallback = $this->queue(...); - $this->runCallback = function () { - if ($this->fiber->isTerminated()) { - $this->createLoopFiber(); - } + $this->runCallback = function (): ?\Closure { + do { + if ($this->fiber->isTerminated()) { + $this->createLoopFiber(); + } - return $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start(); + $result = $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start(); + if ($result) { // Null indicates the loop fiber terminated without suspending. + return $result; + } + } while (\gc_collect_cycles() && !$this->stopped); + + return null; }; } @@ -106,17 +115,14 @@ public function run(): void throw new \Error(\sprintf("Can't call %s() within a fiber (i.e., outside of {main})", __METHOD__)); } - if ($this->fiber->isTerminated()) { - $this->createLoopFiber(); - } - - /** @noinspection PhpUnhandledExceptionInspection */ - $lambda = $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start(); + $lambda = ($this->runCallback)(); if ($lambda) { $lambda(); - throw new \Error('Interrupt from event loop must throw an exception: ' . ClosureHelper::getDescription($lambda)); + throw new \Error( + 'Interrupt from event loop must throw an exception: ' . ClosureHelper::getDescription($lambda) + ); } } diff --git a/test/EventLoopTest.php b/test/EventLoopTest.php index 8f334f2..9aacf59 100644 --- a/test/EventLoopTest.php +++ b/test/EventLoopTest.php @@ -9,6 +9,130 @@ class EventLoopTest extends TestCase { + public function testSuspensionResumptionWithQueueInGarbageCollection(): void + { + $suspension = EventLoop::getSuspension(); + + $class = new class ($suspension) { + public function __construct(public Suspension $suspension) + { + } + public function __destruct() + { + $this->suspension->resume(true); + } + }; + $cycle = [$class, &$cycle]; + unset($class, $cycle); + + $ended = $suspension->suspend(); + + $this->assertTrue($ended); + } + + public function testEventLoopResumptionWithQueueInGarbageCollection(): void + { + $suspension = EventLoop::getSuspension(); + + $class = new class ($suspension) { + public function __construct(public Suspension $suspension) + { + } + public function __destruct() + { + EventLoop::queue($this->suspension->resume(...), true); + } + }; + $cycle = [$class, &$cycle]; + unset($class, $cycle); + + $ended = $suspension->suspend(); + + $this->assertTrue($ended); + } + + + public function testSuspensionResumptionWithQueueInGarbageCollectionNested(): void + { + $suspension = EventLoop::getSuspension(); + + $resumer = new class ($suspension) { + public function __construct(public Suspension $suspension) + { + } + public function __destruct() + { + $this->suspension->resume(true); + } + }; + + $class = new class ($resumer) { + public static ?object $staticReference = null; + public function __construct(object $resumer) + { + self::$staticReference = $resumer; + } + public function __destruct() + { + EventLoop::queue(function () { + $class = self::$staticReference; + $cycle = [$class, &$cycle]; + unset($class, $cycle); + + self::$staticReference = null; + }); + } + }; + $cycle = [$class, &$cycle]; + unset($class, $resumer, $cycle); + + + $ended = $suspension->suspend(); + + $this->assertTrue($ended); + } + + public function testEventLoopResumptionWithQueueInGarbageCollectionNested(): void + { + $suspension = EventLoop::getSuspension(); + + $resumer = new class ($suspension) { + public function __construct(public Suspension $suspension) + { + } + public function __destruct() + { + EventLoop::queue($this->suspension->resume(...), true); + } + }; + + $class = new class ($resumer) { + public static ?object $staticReference = null; + public function __construct(object $resumer) + { + self::$staticReference = $resumer; + } + public function __destruct() + { + EventLoop::queue(function () { + $class = self::$staticReference; + $cycle = [$class, &$cycle]; + unset($class, $cycle); + + self::$staticReference = null; + }); + } + }; + $cycle = [$class, &$cycle]; + unset($class, $resumer, $cycle); + + + $ended = $suspension->suspend(); + + $this->assertTrue($ended); + } + + public function testDelayWithNegativeDelay(): void { $this->expectException(\Error::class);