Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/EventLoop/Internal/DriverSuspension.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

/** @noinspection PhpPropertyOnlyWrittenInspection */

declare(strict_types=1);

namespace Revolt\EventLoop\Internal;
Expand Down Expand Up @@ -46,7 +48,12 @@ public function resume(mixed $value = null): void
$fiber = $this->fiberRef?->get();

if ($fiber) {
($this->queue)($fiber->resume(...), $value);
($this->queue)(static function () use ($fiber, $value): void {
// The fiber may be destroyed with suspension as part of the GC cycle collector.
if (!$fiber->isTerminated()) {
$fiber->resume($value);
}
});
} else {
// Suspend event loop fiber to {main}.
($this->interrupt)(static fn () => $value);
Expand All @@ -72,15 +79,20 @@ public function suspend(): mixed
$this->suspendedFiber = $fiber;

try {
return \Fiber::suspend();
$value = \Fiber::suspend();
$this->suspendedFiber = null;
} catch (\FiberError $exception) {
$this->pending = false;
$this->suspendedFiber = null;
$this->fiberError = $exception;

throw $exception;
} finally {
$this->suspendedFiber = null;
}

// Setting $this->suspendedFiber = null in finally will set the fiber to null if a fiber is destroyed
// as part of a cycle collection, causing an error if the suspension is subsequently resumed.

return $value;
}

// Awaiting from {main}.
Expand Down Expand Up @@ -125,7 +137,12 @@ public function throw(\Throwable $throwable): void
$fiber = $this->fiberRef?->get();

if ($fiber) {
($this->queue)($fiber->throw(...), $throwable);
($this->queue)(static function () use ($fiber, $throwable): void {
// The fiber may be destroyed with suspension as part of the GC cycle collector.
if (!$fiber->isTerminated()) {
$fiber->throw($throwable);
}
});
} else {
// Suspend event loop fiber to {main}.
($this->interrupt)(static fn () => throw $throwable);
Expand Down
41 changes: 41 additions & 0 deletions test/EventLoopTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,45 @@ public function testSuspensionThrowingErrorViaInterrupt(): void
self::assertSame($error, $t->getPrevious());
}
}

public function testFiberDestroyedWhileSuspended(): void
{
$outer = new class (new class ($this) {
private ?Suspension $suspension = null;

public function __construct(public object $outer)
{
}

public function suspend(): void
{
$this->suspension = EventLoop::getSuspension();
$this->suspension->suspend();
}

public function __destruct()
{
echo 'object destroyed';
$suspension = $this->suspension;
$this->suspension = null;
EventLoop::defer(static fn () => $suspension?->resume());
}
}) {
public function __construct(public object $inner)
{
}
};

$inner = $outer->inner;
unset($outer);

EventLoop::queue(static fn () => $inner->suspend());
unset($inner);

EventLoop::queue(\gc_collect_cycles(...));

$this->expectOutputString('object destroyed');

EventLoop::run();
}
}