Skip to content

Commit b2d2448

Browse files
authored
Alternative fix for callbacks queued in destructors (#100)
1 parent 358572c commit b2d2448

File tree

2 files changed

+142
-12
lines changed

2 files changed

+142
-12
lines changed

src/EventLoop/Internal/AbstractDriver.php

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ abstract class AbstractDriver implements Driver
4848

4949
private readonly \Closure $interruptCallback;
5050
private readonly \Closure $queueCallback;
51+
52+
/** @var \Closure():(null|\Closure(): mixed) */
5153
private readonly \Closure $runCallback;
5254

5355
private readonly \stdClass $internalSuspensionMarker;
@@ -87,12 +89,19 @@ public function __construct()
8789
/** @psalm-suppress InvalidArgument */
8890
$this->interruptCallback = $this->setInterrupt(...);
8991
$this->queueCallback = $this->queue(...);
90-
$this->runCallback = function () {
91-
if ($this->fiber->isTerminated()) {
92-
$this->createLoopFiber();
93-
}
92+
$this->runCallback = function (): ?\Closure {
93+
do {
94+
if ($this->fiber->isTerminated()) {
95+
$this->createLoopFiber();
96+
}
9497

95-
return $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start();
98+
$result = $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start();
99+
if ($result) { // Null indicates the loop fiber terminated without suspending.
100+
return $result;
101+
}
102+
} while (\gc_collect_cycles() && !$this->stopped);
103+
104+
return null;
96105
};
97106
}
98107

@@ -106,17 +115,14 @@ public function run(): void
106115
throw new \Error(\sprintf("Can't call %s() within a fiber (i.e., outside of {main})", __METHOD__));
107116
}
108117

109-
if ($this->fiber->isTerminated()) {
110-
$this->createLoopFiber();
111-
}
112-
113-
/** @noinspection PhpUnhandledExceptionInspection */
114-
$lambda = $this->fiber->isStarted() ? $this->fiber->resume() : $this->fiber->start();
118+
$lambda = ($this->runCallback)();
115119

116120
if ($lambda) {
117121
$lambda();
118122

119-
throw new \Error('Interrupt from event loop must throw an exception: ' . ClosureHelper::getDescription($lambda));
123+
throw new \Error(
124+
'Interrupt from event loop must throw an exception: ' . ClosureHelper::getDescription($lambda)
125+
);
120126
}
121127
}
122128

test/EventLoopTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,130 @@
99

1010
class EventLoopTest extends TestCase
1111
{
12+
public function testSuspensionResumptionWithQueueInGarbageCollection(): void
13+
{
14+
$suspension = EventLoop::getSuspension();
15+
16+
$class = new class ($suspension) {
17+
public function __construct(public Suspension $suspension)
18+
{
19+
}
20+
public function __destruct()
21+
{
22+
$this->suspension->resume(true);
23+
}
24+
};
25+
$cycle = [$class, &$cycle];
26+
unset($class, $cycle);
27+
28+
$ended = $suspension->suspend();
29+
30+
$this->assertTrue($ended);
31+
}
32+
33+
public function testEventLoopResumptionWithQueueInGarbageCollection(): void
34+
{
35+
$suspension = EventLoop::getSuspension();
36+
37+
$class = new class ($suspension) {
38+
public function __construct(public Suspension $suspension)
39+
{
40+
}
41+
public function __destruct()
42+
{
43+
EventLoop::queue($this->suspension->resume(...), true);
44+
}
45+
};
46+
$cycle = [$class, &$cycle];
47+
unset($class, $cycle);
48+
49+
$ended = $suspension->suspend();
50+
51+
$this->assertTrue($ended);
52+
}
53+
54+
55+
public function testSuspensionResumptionWithQueueInGarbageCollectionNested(): void
56+
{
57+
$suspension = EventLoop::getSuspension();
58+
59+
$resumer = new class ($suspension) {
60+
public function __construct(public Suspension $suspension)
61+
{
62+
}
63+
public function __destruct()
64+
{
65+
$this->suspension->resume(true);
66+
}
67+
};
68+
69+
$class = new class ($resumer) {
70+
public static ?object $staticReference = null;
71+
public function __construct(object $resumer)
72+
{
73+
self::$staticReference = $resumer;
74+
}
75+
public function __destruct()
76+
{
77+
EventLoop::queue(function () {
78+
$class = self::$staticReference;
79+
$cycle = [$class, &$cycle];
80+
unset($class, $cycle);
81+
82+
self::$staticReference = null;
83+
});
84+
}
85+
};
86+
$cycle = [$class, &$cycle];
87+
unset($class, $resumer, $cycle);
88+
89+
90+
$ended = $suspension->suspend();
91+
92+
$this->assertTrue($ended);
93+
}
94+
95+
public function testEventLoopResumptionWithQueueInGarbageCollectionNested(): void
96+
{
97+
$suspension = EventLoop::getSuspension();
98+
99+
$resumer = new class ($suspension) {
100+
public function __construct(public Suspension $suspension)
101+
{
102+
}
103+
public function __destruct()
104+
{
105+
EventLoop::queue($this->suspension->resume(...), true);
106+
}
107+
};
108+
109+
$class = new class ($resumer) {
110+
public static ?object $staticReference = null;
111+
public function __construct(object $resumer)
112+
{
113+
self::$staticReference = $resumer;
114+
}
115+
public function __destruct()
116+
{
117+
EventLoop::queue(function () {
118+
$class = self::$staticReference;
119+
$cycle = [$class, &$cycle];
120+
unset($class, $cycle);
121+
122+
self::$staticReference = null;
123+
});
124+
}
125+
};
126+
$cycle = [$class, &$cycle];
127+
unset($class, $resumer, $cycle);
128+
129+
130+
$ended = $suspension->suspend();
131+
132+
$this->assertTrue($ended);
133+
}
134+
135+
12136
public function testDelayWithNegativeDelay(): void
13137
{
14138
$this->expectException(\Error::class);

0 commit comments

Comments
 (0)