Skip to content

Commit c7a6f2a

Browse files
authored
Merge pull request #28 from true-async/23-investigate-zombie-deadlocks-behavior
23 investigate zombie deadlocks behavior
2 parents cd415ba + 8ca3c01 commit c7a6f2a

12 files changed

+341
-14
lines changed

async_API.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ void async_await_futures(
10561056

10571057
// If the await on futures has completed and
10581058
// the automatic cancellation mode for pending coroutines is active.
1059+
// !Note! that at this point we are finally awaiting the completion of all cancelled Futures.
10591060
if (await_context->cancel_on_exit) {
10601061
async_cancel_awaited_futures(await_context, futures);
10611062
}

scheduler.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,11 @@ void async_scheduler_launch(void)
523523
return;
524524
}
525525

526+
if (EG(active_fiber)) {
527+
async_throw_error("The True Async Scheduler cannot be started from within a Fiber");
528+
return;
529+
}
530+
526531
if (false == zend_async_reactor_is_enabled()) {
527532
async_throw_error("The scheduler cannot be started without the Reactor");
528533
return;
@@ -886,7 +891,7 @@ void async_scheduler_coroutine_suspend(zend_fiber_transfer *transfer)
886891
if (UNEXPECTED(
887892
false == has_handles
888893
&& false == is_next_coroutine
889-
&& ZEND_ASYNC_ACTIVE_COROUTINE_COUNT > 0
894+
&& zend_hash_num_elements(&ASYNC_G(coroutines)) > 0
890895
&& circular_buffer_is_empty(&ASYNC_G(microtasks))
891896
&& resolve_deadlocks()
892897
)) {
@@ -950,7 +955,7 @@ void async_scheduler_main_loop(void)
950955
if (UNEXPECTED(
951956
false == has_handles
952957
&& false == was_executed
953-
&& ZEND_ASYNC_ACTIVE_COROUTINE_COUNT > 0
958+
&& zend_hash_num_elements(&ASYNC_G(coroutines)) > 0
954959
&& circular_buffer_is_empty(&ASYNC_G(coroutine_queue))
955960
&& circular_buffer_is_empty(&ASYNC_G(microtasks))
956961
&& resolve_deadlocks()

scope.c

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,10 @@ async_scope_remove_coroutine(async_scope_t *scope, async_coroutine_t *coroutine)
6363
for (uint32_t i = 0; i < vector->length; ++i) {
6464
if (vector->data[i] == coroutine) {
6565
// Decrement active coroutines count if coroutine was active
66-
if (false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine)) {
67-
if (scope->active_coroutines_count > 0) {
68-
scope->active_coroutines_count--;
69-
} else if (scope->zombie_coroutines_count > 0) {
70-
scope->zombie_coroutines_count--;
71-
}
66+
if (false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine) && scope->active_coroutines_count > 0) {
67+
scope->active_coroutines_count--;
68+
} else if (scope->zombie_coroutines_count > 0) {
69+
scope->zombie_coroutines_count--;
7270
}
7371

7472
vector->data[i] = vector->data[--vector->length];
@@ -1087,6 +1085,12 @@ static zend_string* scope_info(zend_async_event_t *event)
10871085

10881086
static void scope_dispose(zend_async_event_t *scope_event)
10891087
{
1088+
async_scope_t *scope = (async_scope_t *) scope_event;
1089+
1090+
if (ZEND_ASYNC_SCOPE_IS_DISPOSING(&scope->scope)) {
1091+
return;
1092+
}
1093+
10901094
if (ZEND_ASYNC_EVENT_REF(scope_event) > 1) {
10911095
ZEND_ASYNC_EVENT_DEL_REF(scope_event);
10921096
return;
@@ -1096,16 +1100,31 @@ static void scope_dispose(zend_async_event_t *scope_event)
10961100
ZEND_ASYNC_EVENT_DEL_REF(scope_event);
10971101
}
10981102

1099-
async_scope_t *scope = (async_scope_t *) scope_event;
1103+
ZEND_ASYNC_SCOPE_SET_DISPOSING(&scope->scope);
11001104

11011105
ZEND_ASSERT(scope->coroutines.length == 0 && scope->scope.scopes.length == 0
11021106
&& "Scope should be empty before disposal");
11031107

1108+
zend_object *critical_exception = NULL;
1109+
1110+
//
1111+
// Notifying subscribers one last time that the scope has been definitively completed.
1112+
//
1113+
ZEND_ASYNC_CALLBACKS_NOTIFY(scope_event, NULL, NULL);
1114+
zend_async_callbacks_free(&scope->scope.event);
1115+
if (UNEXPECTED(EG(exception))) {
1116+
critical_exception = zend_exception_merge(critical_exception, true, true);
1117+
}
1118+
11041119
if (scope->finally_handlers != NULL
11051120
&& zend_hash_num_elements(scope->finally_handlers) > 0
11061121
&& async_scope_call_finally_handlers(scope)) {
11071122
// If finally handlers were called, we don't dispose the scope yet
11081123
ZEND_ASYNC_EVENT_ADD_REF(&scope->scope.event);
1124+
if (critical_exception) {
1125+
async_spawn_and_throw(critical_exception, &scope->scope, 0);
1126+
}
1127+
ZEND_ASYNC_SCOPE_CLR_DISPOSING(&scope->scope);
11091128
return;
11101129
}
11111130

@@ -1153,6 +1172,10 @@ static void scope_dispose(zend_async_event_t *scope_event)
11531172
async_scope_free_coroutines(scope);
11541173
zend_async_scope_free_children(&scope->scope);
11551174
efree(scope);
1175+
1176+
if (critical_exception != NULL) {
1177+
async_rethrow_exception(critical_exception);
1178+
}
11561179
}
11571180

11581181
zend_async_scope_t * async_new_scope(zend_async_scope_t * parent_scope, const bool with_zend_object)
@@ -1181,6 +1204,11 @@ zend_async_scope_t * async_new_scope(zend_async_scope_t * parent_scope, const bo
11811204
scope->scope.parent_scope = parent_scope;
11821205
zend_async_event_t *event = &scope->scope.event;
11831206

1207+
// Inherit safely disposal flag from parent scope or set it to true if parent scope is NULL
1208+
if (parent_scope == NULL || ZEND_ASYNC_SCOPE_IS_DISPOSE_SAFELY(parent_scope)) {
1209+
ZEND_ASYNC_SCOPE_SET_DISPOSE_SAFELY(&scope->scope);
1210+
}
1211+
11841212
event->ref_count = 1; // Initialize reference count
11851213

11861214
scope->scope.before_coroutine_enqueue = scope_before_coroutine_enqueue;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
Fiber created first, then spawn operation - should detect incompatible context
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Test: Fiber first, then spawn\n";
10+
11+
try {
12+
$fiber = new Fiber(function() {
13+
echo "Inside Fiber\n";
14+
15+
// This should cause issues - spawning from within a Fiber context
16+
$coroutine = spawn(function() {
17+
echo "Inside spawned coroutine from Fiber\n";
18+
suspend();
19+
echo "Coroutine completed\n";
20+
});
21+
22+
echo "Fiber attempting to continue after spawn\n";
23+
Fiber::suspend("fiber suspended");
24+
echo "Fiber resumed\n";
25+
26+
return "fiber done";
27+
});
28+
29+
echo "Starting Fiber\n";
30+
$result = $fiber->start();
31+
echo "Fiber suspended with: " . $result . "\n";
32+
33+
echo "Resuming Fiber\n";
34+
$result = $fiber->resume("resume value");
35+
echo "Fiber returned: " . $result . "\n";
36+
37+
} catch (Async\AsyncException $e) {
38+
echo "Async exception caught: " . $e->getMessage() . "\n";
39+
} catch (Exception $e) {
40+
echo "Exception caught: " . $e->getMessage() . "\n";
41+
}
42+
43+
echo "Test completed\n";
44+
?>
45+
--EXPECTF--
46+
Test: Fiber first, then spawn
47+
Starting Fiber
48+
Inside Fiber
49+
Async exception caught: Cannot spawn a coroutine when async is disabled
50+
Test completed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
--TEST--
2+
Spawn coroutine first, then create Fiber - should detect context conflicts
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
use function Async\await;
9+
10+
echo "Test: Spawn first, then Fiber\n";
11+
12+
try {
13+
// First spawn a coroutine that will suspend and wait
14+
$coroutine = spawn(function() {
15+
echo "Coroutine started\n";
16+
suspend(); // This activates the async scheduler
17+
echo "Coroutine resumed\n";
18+
return "coroutine result";
19+
});
20+
21+
echo "Coroutine spawned, now creating Fiber\n";
22+
23+
// Now try to create and use a Fiber while async scheduler is active
24+
$fiber = new Fiber(function() {
25+
echo "Inside Fiber - this should conflict with active scheduler\n";
26+
27+
// Try to interact with the active coroutine from within Fiber
28+
// This creates a context conflict
29+
Fiber::suspend("fiber suspended");
30+
31+
echo "Fiber resumed\n";
32+
return "fiber done";
33+
});
34+
35+
echo "Starting Fiber\n";
36+
$fiberResult = $fiber->start();
37+
echo "Fiber suspended with: " . $fiberResult . "\n";
38+
39+
echo "Resuming Fiber\n";
40+
$fiberResult = $fiber->resume("resume data");
41+
echo "Fiber completed with: " . $fiberResult . "\n";
42+
43+
echo "Getting coroutine result\n";
44+
$coroutineResult = await($coroutine);
45+
echo "Coroutine completed with: " . $coroutineResult . "\n";
46+
47+
} catch (Error $e) {
48+
echo "Error caught: " . $e->getMessage() . "\n";
49+
} catch (Exception $e) {
50+
echo "Exception caught: " . $e->getMessage() . "\n";
51+
}
52+
53+
echo "Test completed\n";
54+
?>
55+
--EXPECTF--
56+
Test: Spawn first, then Fiber
57+
Coroutine spawned, now creating Fiber
58+
Error caught: Cannot create a fiber while an True Async is active
59+
Test completed
60+
Coroutine started
61+
Coroutine resumed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
--TEST--
2+
Fiber and spawn operations in destructors - memory management conflicts
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
use function Async\await;
9+
10+
echo "Test: Fiber and spawn in destructors\n";
11+
12+
class FiberSpawner {
13+
private $name;
14+
15+
public function __construct($name) {
16+
$this->name = $name;
17+
echo "Created: {$this->name}\n";
18+
}
19+
20+
public function __destruct() {
21+
echo "Destructing: {$this->name}\n";
22+
23+
try {
24+
if ($this->name === 'FiberInDestructor') {
25+
// Create Fiber in destructor
26+
$fiber = new Fiber(function() {
27+
echo "Fiber running in destructor\n";
28+
Fiber::suspend("destructor fiber");
29+
echo "Fiber resumed in destructor\n";
30+
return "destructor done";
31+
});
32+
33+
echo "Starting fiber in destructor\n";
34+
$result = $fiber->start();
35+
echo "Fiber suspended with: " . $result . "\n";
36+
37+
$result = $fiber->resume("resume in destructor");
38+
echo "Fiber completed with: " . $result . "\n";
39+
40+
} elseif ($this->name === 'SpawnInDestructor') {
41+
// Spawn coroutine in destructor
42+
echo "Spawning coroutine in destructor\n";
43+
$coroutine = spawn(function() {
44+
echo "Coroutine running in destructor\n";
45+
suspend();
46+
echo "Coroutine resumed in destructor\n";
47+
return "destructor coroutine done";
48+
});
49+
50+
echo "Waiting for coroutine in destructor\n";
51+
$result = await($coroutine);
52+
echo "Coroutine completed with: " . $result . "\n";
53+
}
54+
} catch (Error $e) {
55+
echo "Error in destructor: " . $e->getMessage() . "\n";
56+
} catch (Exception $e) {
57+
echo "Exception in destructor: " . $e->getMessage() . "\n";
58+
}
59+
60+
echo "Destructor finished: {$this->name}\n";
61+
}
62+
}
63+
64+
try {
65+
echo "Creating objects that will spawn/fiber in destructors\n";
66+
67+
$obj1 = new FiberSpawner('FiberInDestructor');
68+
$obj2 = new FiberSpawner('SpawnInDestructor');
69+
70+
echo "Starting some async operations\n";
71+
$mainCoroutine = spawn(function() {
72+
echo "Main coroutine running\n";
73+
suspend();
74+
echo "Main coroutine resumed\n";
75+
return "main done";
76+
});
77+
78+
// Force destruction by unsetting
79+
echo "Unsetting objects to trigger destructors\n";
80+
unset($obj1);
81+
unset($obj2);
82+
83+
echo "Completing main coroutine\n";
84+
$result = await($mainCoroutine);
85+
echo "Main coroutine result: " . $result . "\n";
86+
87+
} catch (Error $e) {
88+
echo "Error caught: " . $e->getMessage() . "\n";
89+
} catch (Exception $e) {
90+
echo "Exception caught: " . $e->getMessage() . "\n";
91+
}
92+
93+
echo "Test completed\n";
94+
?>
95+
--EXPECTF--
96+
Test: Fiber and spawn in destructors
97+
Creating objects that will spawn/fiber in destructors
98+
Created: FiberInDestructor
99+
Created: SpawnInDestructor
100+
Starting some async operations
101+
Unsetting objects to trigger destructors
102+
Destructing: FiberInDestructor
103+
Error in destructor: Cannot create a fiber while an True Async is active
104+
Destructor finished: FiberInDestructor
105+
Destructing: SpawnInDestructor
106+
Spawning coroutine in destructor
107+
Waiting for coroutine in destructor
108+
Main coroutine running
109+
Coroutine running in destructor
110+
Main coroutine resumed
111+
Coroutine resumed in destructor
112+
Coroutine completed with: destructor coroutine done
113+
Destructor finished: SpawnInDestructor
114+
Completing main coroutine
115+
Main coroutine result: main done
116+
Test completed

0 commit comments

Comments
 (0)