Skip to content

Commit 9d34292

Browse files
committed
#23: * Adjustment of Scope: By default, the Scope is created in safely dispose mode.
1 parent 1270263 commit 9d34292

8 files changed

+108
-14
lines changed

scheduler.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,7 @@ void async_scheduler_coroutine_suspend(zend_fiber_transfer *transfer)
891891
if (UNEXPECTED(
892892
false == has_handles
893893
&& false == is_next_coroutine
894-
&& ZEND_ASYNC_ACTIVE_COROUTINE_COUNT > 0
894+
&& zend_hash_num_elements(&ASYNC_G(coroutines)) > 0
895895
&& circular_buffer_is_empty(&ASYNC_G(microtasks))
896896
&& resolve_deadlocks()
897897
)) {
@@ -955,7 +955,7 @@ void async_scheduler_main_loop(void)
955955
if (UNEXPECTED(
956956
false == has_handles
957957
&& false == was_executed
958-
&& ZEND_ASYNC_ACTIVE_COROUTINE_COUNT > 0
958+
&& zend_hash_num_elements(&ASYNC_G(coroutines)) > 0
959959
&& circular_buffer_is_empty(&ASYNC_G(coroutine_queue))
960960
&& circular_buffer_is_empty(&ASYNC_G(microtasks))
961961
&& 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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
Deadlock - Deadlock is an operation after coroutines are cancelled, when they are already zombies.
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\await;
8+
use function Async\suspend;
9+
use Async\Scope;
10+
11+
echo "start\n";
12+
13+
$scope = new Scope();
14+
15+
$coroutine1 = null;
16+
$coroutine2 = null;
17+
18+
$coroutine1 = $scope->spawn(function() use (&$coroutine2) {
19+
echo "coroutine1 running\n";
20+
suspend();
21+
22+
try {
23+
await($coroutine2);
24+
} catch (Throwable $e) {
25+
echo "Caught exception: " . $e->getMessage() . "\n";
26+
}
27+
28+
suspend();
29+
30+
echo "coroutine1 finished\n";
31+
});
32+
33+
$coroutine2 = $scope->spawn(function() use ($coroutine1) {
34+
echo "coroutine2 running\n";
35+
suspend();
36+
try {
37+
await($coroutine1);
38+
} catch (Throwable $e) {
39+
echo "Caught exception: " . $e->getMessage() . "\n";
40+
}
41+
42+
suspend();
43+
44+
echo "coroutine2 finished\n";
45+
});
46+
47+
suspend(); // Suspend the main coroutine to allow the others to run
48+
$scope->dispose(); // This will cancel the coroutines, making them zombies
49+
50+
echo "end\n";
51+
?>
52+
--EXPECTF--
53+
start
54+
coroutine1 running
55+
coroutine2 running
56+
end
57+
58+
Warning: no active coroutines, deadlock detected. Coroutines in waiting: 2 in %s on line %d
59+
60+
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in %s on line %d
61+
62+
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in %s on line %d
63+
Caught exception: Deadlock detected
64+
coroutine1 finished
65+
coroutine2 finished

tests/scope/024-scope_awaitAfterCancellation_basic.phpt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ coroutine1 started
6060
coroutine2 started
6161
scope cancelled
6262
external waiting after cancellation
63+
coroutine1 finished
64+
coroutine2 finished
6365
awaitAfterCancellation completed
6466
scope finished: true
6567
scope closed: true

tests/scope/025-scope_awaitAfterCancellation_error_handler.phpt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@ error coroutine started
7171
normal coroutine started
7272
external waiting with error handler
7373
scope cancel
74-
coroutine cancelled
7574
awaitAfterCancellation with handler started
76-
error handler called: Coroutine error after cancellation
75+
normal coroutine finished
7776
awaitAfterCancellation with handler completed
7877
scope finished: true
7978
end

tests/scope/026-scope_cancel_with_active_coroutines.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use Async\Scope;
1010
echo "start\n";
1111

1212
// Test comprehensive cancellation behavior
13-
$scope = Scope::inherit();
13+
$scope = Scope::inherit()->asNotSafely();
1414

1515
$coroutine1 = $scope->spawn(function() {
1616
echo "coroutine1 started\n";

tests/scope/029-scope_complex_tree_cancellation.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use function Async\await;
1010
echo "start\n";
1111

1212
// Create complex scope tree: parent -> child -> grandchild -> great-grandchild
13-
$parent_scope = new \Async\Scope();
13+
$parent_scope = new \Async\Scope()->asNotSafely();
1414
$child_scope = \Async\Scope::inherit($parent_scope);
1515
$grandchild_scope = \Async\Scope::inherit($child_scope);
1616
$great_grandchild_scope = \Async\Scope::inherit($grandchild_scope);

tests/scope/033-scope_cancellation_finally_handlers.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use function Async\await;
99

1010
echo "start\n";
1111

12-
$scope = new \Async\Scope();
12+
$scope = new \Async\Scope()->asNotSafely();
1313

1414
// Spawn coroutine with finally handlers
1515
$coroutine_with_finally = $scope->spawn(function() {

0 commit comments

Comments
 (0)