Skip to content

Commit 4a69379

Browse files
authored
Check if fiber was destroyed before resuming (#84)
Do not set $suspendedFiber to null when fiber is destroyed.
1 parent c5a663b commit 4a69379

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

src/EventLoop/Internal/DriverSuspension.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
/** @noinspection PhpPropertyOnlyWrittenInspection */
4+
35
declare(strict_types=1);
46

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

4850
if ($fiber) {
49-
($this->queue)($fiber->resume(...), $value);
51+
($this->queue)(static function () use ($fiber, $value): void {
52+
// The fiber may be destroyed with suspension as part of the GC cycle collector.
53+
if (!$fiber->isTerminated()) {
54+
$fiber->resume($value);
55+
}
56+
});
5057
} else {
5158
// Suspend event loop fiber to {main}.
5259
($this->interrupt)(static fn () => $value);
@@ -72,15 +79,20 @@ public function suspend(): mixed
7279
$this->suspendedFiber = $fiber;
7380

7481
try {
75-
return \Fiber::suspend();
82+
$value = \Fiber::suspend();
83+
$this->suspendedFiber = null;
7684
} catch (\FiberError $exception) {
7785
$this->pending = false;
86+
$this->suspendedFiber = null;
7887
$this->fiberError = $exception;
7988

8089
throw $exception;
81-
} finally {
82-
$this->suspendedFiber = null;
8390
}
91+
92+
// Setting $this->suspendedFiber = null in finally will set the fiber to null if a fiber is destroyed
93+
// as part of a cycle collection, causing an error if the suspension is subsequently resumed.
94+
95+
return $value;
8496
}
8597

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

127139
if ($fiber) {
128-
($this->queue)($fiber->throw(...), $throwable);
140+
($this->queue)(static function () use ($fiber, $throwable): void {
141+
// The fiber may be destroyed with suspension as part of the GC cycle collector.
142+
if (!$fiber->isTerminated()) {
143+
$fiber->throw($throwable);
144+
}
145+
});
129146
} else {
130147
// Suspend event loop fiber to {main}.
131148
($this->interrupt)(static fn () => throw $throwable);

test/EventLoopTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,45 @@ public function testSuspensionThrowingErrorViaInterrupt(): void
299299
self::assertSame($error, $t->getPrevious());
300300
}
301301
}
302+
303+
public function testFiberDestroyedWhileSuspended(): void
304+
{
305+
$outer = new class (new class ($this) {
306+
private ?Suspension $suspension = null;
307+
308+
public function __construct(public object $outer)
309+
{
310+
}
311+
312+
public function suspend(): void
313+
{
314+
$this->suspension = EventLoop::getSuspension();
315+
$this->suspension->suspend();
316+
}
317+
318+
public function __destruct()
319+
{
320+
echo 'object destroyed';
321+
$suspension = $this->suspension;
322+
$this->suspension = null;
323+
EventLoop::defer(static fn () => $suspension?->resume());
324+
}
325+
}) {
326+
public function __construct(public object $inner)
327+
{
328+
}
329+
};
330+
331+
$inner = $outer->inner;
332+
unset($outer);
333+
334+
EventLoop::queue(static fn () => $inner->suspend());
335+
unset($inner);
336+
337+
EventLoop::queue(\gc_collect_cycles(...));
338+
339+
$this->expectOutputString('object destroyed');
340+
341+
EventLoop::run();
342+
}
302343
}

0 commit comments

Comments
 (0)