Skip to content

Commit 3c56af9

Browse files
committed
Allow fiber switching during destructor execution
Fiber switching was disabled during destructor execution due to conflicts with the garbage collector. This unfortunately introduces a function color problem: destructors can not call functions that may switch Fibers. In this change we update the GC so that Fiber switching during GC is safe. In turn we allow Fiber switching during destrutor execution. The GC executes destructors in a dedicated Fiber. If a destructor suspends, the Fiber is owned by userland and a new dedicated Fiber is created to execute the remaining destructors. Destructor suspension results in a resurection of the object, which is handled as usual: The object is not considered garbage anymore, but may be collected in a later run. When the GC is executed in the main context (not in a Fiber), then destructors are executed in the main context as well because there is no risk of conflicting with GC in this case (main context can not suspend). Fixes GH-11389 Closes GH-13460
1 parent 7c6ff87 commit 3c56af9

22 files changed

+688
-210
lines changed

Zend/tests/bug69446.phpt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ unset($foo);
2323
gc_collect_cycles();
2424
var_dump($bar);
2525
?>
26-
--EXPECT--
27-
object(bad)#2 (2) {
26+
--EXPECTF--
27+
object(bad)#%d (2) {
2828
["x"]=>
29-
object(stdClass)#3 (0) {
29+
object(stdClass)#%d (0) {
3030
}
3131
["y"]=>
32-
object(stdClass)#4 (0) {
32+
object(stdClass)#%d (0) {
3333
}
3434
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
Fibers in destructors 001: Suspend in destructor
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public static $counter = 0;
12+
public $self;
13+
public function __construct() {
14+
$this->self = $this;
15+
}
16+
public function __destruct() {
17+
$id = self::$counter++;
18+
printf("%d: Start destruct\n", $id);
19+
if ($id === 0) {
20+
global $f2;
21+
$f2 = Fiber::getCurrent();
22+
Fiber::suspend(new stdClass);
23+
}
24+
printf("%d: End destruct\n", $id);
25+
}
26+
}
27+
28+
$f = new Fiber(function () {
29+
global $f2;
30+
new Cycle();
31+
new Cycle();
32+
new Cycle();
33+
new Cycle();
34+
new Cycle();
35+
gc_collect_cycles();
36+
$f2->resume();
37+
});
38+
39+
$f->start();
40+
41+
?>
42+
--EXPECT--
43+
0: Start destruct
44+
1: Start destruct
45+
1: End destruct
46+
2: Start destruct
47+
2: End destruct
48+
3: Start destruct
49+
3: End destruct
50+
4: Start destruct
51+
4: End destruct
52+
0: End destruct
53+
Shutdown
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
Fibers in destructors 002: Start in destructor
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public static $counter = 0;
12+
public $self;
13+
public function __construct() {
14+
$this->self = $this;
15+
}
16+
public function __destruct() {
17+
$id = self::$counter++;
18+
printf("%d: Start destruct\n", $id);
19+
$f = new Fiber(function () { });
20+
$f->start();
21+
printf("%d: End destruct\n", $id);
22+
}
23+
}
24+
25+
new Cycle();
26+
new Cycle();
27+
gc_collect_cycles();
28+
29+
?>
30+
--EXPECT--
31+
0: Start destruct
32+
0: End destruct
33+
1: Start destruct
34+
1: End destruct
35+
Shutdown
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Fibers in destructors 003: Resume in destructor
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public static $counter = 0;
12+
public $self;
13+
public function __construct() {
14+
$this->self = $this;
15+
}
16+
public function __destruct() {
17+
$id = self::$counter++;
18+
printf("%d: Start destruct\n", $id);
19+
global $f;
20+
$f->resume();
21+
printf("%d: End destruct\n", $id);
22+
}
23+
}
24+
25+
$f = new Fiber(function () {
26+
while (true) {
27+
Fiber::suspend();
28+
}
29+
});
30+
$f->start();
31+
32+
new Cycle();
33+
new Cycle();
34+
gc_collect_cycles();
35+
36+
?>
37+
--EXPECT--
38+
0: Start destruct
39+
0: End destruct
40+
1: Start destruct
41+
1: End destruct
42+
Shutdown
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
--TEST--
2+
Fibers in destructors 004: Suspend and throw in destructor
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public static $counter = 0;
12+
public $self;
13+
public function __construct() {
14+
$this->self = $this;
15+
}
16+
public function __destruct() {
17+
$id = self::$counter++;
18+
printf("%d: Start destruct\n", $id);
19+
if ($id === 0) {
20+
global $f2;
21+
$f2 = Fiber::getCurrent();
22+
Fiber::suspend(new stdClass);
23+
}
24+
printf("%d: End destruct\n", $id);
25+
throw new \Exception(sprintf("%d exception", $id));
26+
}
27+
}
28+
29+
$f = new Fiber(function () {
30+
global $f2;
31+
new Cycle();
32+
new Cycle();
33+
new Cycle();
34+
try {
35+
gc_collect_cycles();
36+
} catch (\Exception $e) {
37+
echo $e, "\n";
38+
}
39+
$f2->resume();
40+
});
41+
42+
$f->start();
43+
44+
?>
45+
--EXPECTF--
46+
0: Start destruct
47+
1: Start destruct
48+
1: End destruct
49+
2: Start destruct
50+
2: End destruct
51+
Exception: 1 exception in %s:%d
52+
Stack trace:
53+
#0 [internal function]: Cycle->__destruct()
54+
#1 [internal function]: gc_destructor_fiber()
55+
#2 %s(%d): gc_collect_cycles()
56+
#3 [internal function]: {closure:%s:%d}()
57+
#4 %s(%d): Fiber->start()
58+
#5 {main}
59+
60+
Next Exception: 2 exception in %s:%d
61+
Stack trace:
62+
#0 [internal function]: Cycle->__destruct()
63+
#1 [internal function]: gc_destructor_fiber()
64+
#2 %s(%d): gc_collect_cycles()
65+
#3 [internal function]: {closure:%s:%d}()
66+
#4 %s(%d): Fiber->start()
67+
#5 {main}
68+
0: End destruct
69+
70+
Fatal error: Uncaught Exception: 0 exception in %s:%d
71+
Stack trace:
72+
#0 [internal function]: Cycle->__destruct()
73+
#1 [internal function]: gc_destructor_fiber()
74+
#2 %s(%d): Fiber->resume()
75+
#3 [internal function]: {closure:%s:%d}()
76+
#4 %s(%d): Fiber->start()
77+
#5 {main}
78+
thrown in %s on line %d
79+
Shutdown
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
--TEST--
2+
Fibers in destructors 005: Suspended and not resumed destructor
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public $self;
12+
public function __construct(public int $id) {
13+
$this->self = $this;
14+
}
15+
public function __destruct() {
16+
printf("%d: Start destruct\n", $this->id);
17+
try {
18+
if ($this->id === 0) {
19+
/* Fiber will be collected by GC because it's not referenced */
20+
Fiber::suspend(new stdClass);
21+
} else if ($this->id === 1) {
22+
/* Fiber will be dtor during shutdown */
23+
global $f2;
24+
$f2 = Fiber::getCurrent();
25+
Fiber::suspend(new stdClass);
26+
}
27+
} finally {
28+
printf("%d: End destruct\n", $this->id);
29+
}
30+
}
31+
}
32+
33+
$refs = [];
34+
$f = new Fiber(function () use (&$refs) {
35+
$refs[] = WeakReference::create(new Cycle(0));
36+
$refs[] = WeakReference::create(new Cycle(1));
37+
$refs[] = WeakReference::create(new Cycle(2));
38+
gc_collect_cycles();
39+
});
40+
41+
$f->start();
42+
43+
gc_collect_cycles();
44+
45+
foreach ($refs as $id => $ref) {
46+
printf("%d: %s\n", $id, $ref->get() ? 'Live' : 'Collected');
47+
}
48+
49+
?>
50+
--EXPECT--
51+
2: Start destruct
52+
2: End destruct
53+
0: Start destruct
54+
0: End destruct
55+
1: Start destruct
56+
0: Collected
57+
1: Live
58+
2: Collected
59+
Shutdown
60+
1: End destruct
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
Fibers in destructors 006: multiple GC runs
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public static $counter = 0;
12+
public $self;
13+
public function __construct() {
14+
$this->self = $this;
15+
}
16+
public function __destruct() {
17+
$id = self::$counter++;
18+
printf("%d: Start destruct\n", $id);
19+
if ($id === 0) {
20+
global $f2;
21+
$f2 = Fiber::getCurrent();
22+
Fiber::suspend(new stdClass);
23+
}
24+
printf("%d: End destruct\n", $id);
25+
}
26+
}
27+
28+
$f = new Fiber(function () {
29+
new Cycle();
30+
new Cycle();
31+
gc_collect_cycles();
32+
});
33+
34+
$f->start();
35+
36+
new Cycle();
37+
new Cycle();
38+
gc_collect_cycles();
39+
40+
$f2->resume();
41+
42+
?>
43+
--EXPECT--
44+
0: Start destruct
45+
1: Start destruct
46+
1: End destruct
47+
2: Start destruct
48+
2: End destruct
49+
3: Start destruct
50+
3: End destruct
51+
0: End destruct
52+
Shutdown
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
Fibers in destructors 007: scope destructor
3+
--FILE--
4+
<?php
5+
6+
register_shutdown_function(function () {
7+
printf("Shutdown\n");
8+
});
9+
10+
class Cycle {
11+
public static $counter = 0;
12+
public function __destruct() {
13+
$id = self::$counter++;
14+
printf("%d: Start destruct\n", $id);
15+
switch ($id) {
16+
case 0:
17+
global $f2;
18+
$f2 = Fiber::getCurrent();
19+
Fiber::suspend(new stdClass);
20+
break;
21+
case 1:
22+
$f3 = new Fiber(function () use ($id) {
23+
printf("%d: Fiber\n", $id);
24+
});
25+
$f3->start();
26+
break;
27+
case 2:
28+
global $f2;
29+
$f2->resume();
30+
break;
31+
}
32+
printf("%d: End destruct\n", $id);
33+
}
34+
}
35+
36+
$f = new Fiber(function () {
37+
new Cycle();
38+
});
39+
40+
$f->start();
41+
42+
new Cycle();
43+
new Cycle();
44+
45+
?>
46+
--EXPECT--
47+
0: Start destruct
48+
1: Start destruct
49+
1: Fiber
50+
1: End destruct
51+
2: Start destruct
52+
0: End destruct
53+
2: End destruct
54+
Shutdown

0 commit comments

Comments
 (0)