Skip to content

Commit 781c826

Browse files
authored
Merge pull request #21 from true-async/8-bailout-tests
8 bailout tests
2 parents 192d920 + 9e12997 commit 781c826

21 files changed

+867
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
## [0.3.0] - TBD
1111

1212
### Added
13+
- **Bailout Tests**: Added 15 tests covering memory exhaustion and stack overflow scenarios in async operations
1314
- **Garbage Collection Support**: Implemented comprehensive GC handlers for async objects
1415
- Added `async_coroutine_object_gc()` function to track all ZVALs in coroutine structures
1516
- Added `async_scope_object_gc()` function to track ZVALs in scope structures

async_API.c

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,6 @@ zend_coroutine_t *spawn(zend_async_scope_t *scope, zend_object * scope_provider,
196196
return &coroutine->coroutine;
197197
}
198198

199-
static void graceful_shutdown(void)
200-
{
201-
start_graceful_shutdown();
202-
}
203-
204199
static void engine_shutdown(void)
205200
{
206201
ZEND_ASYNC_REACTOR_SHUTDOWN();
@@ -906,7 +901,7 @@ void async_api_register(void)
906901
async_coroutine_resume,
907902
async_coroutine_cancel,
908903
async_spawn_and_throw,
909-
graceful_shutdown,
904+
start_graceful_shutdown,
910905
get_coroutines,
911906
add_microtask,
912907
get_awaiting_info,

coroutine.c

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -626,31 +626,31 @@ void async_coroutine_finalize(zend_fiber_transfer *transfer, async_coroutine_t *
626626
async_rethrow_exception(exception);
627627
}
628628

629-
if (EG(exception)) {
630-
if (!(coroutine->flags & ZEND_FIBER_FLAG_DESTROYED)
631-
|| !(zend_is_graceful_exit(EG(exception)) || zend_is_unwind_exit(EG(exception)))
632-
) {
633-
coroutine->flags |= ZEND_FIBER_FLAG_THREW;
634-
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_ERROR;
635-
636-
ZVAL_OBJ_COPY(&transfer->value, EG(exception));
637-
}
638-
639-
zend_clear_exception();
640-
}
641629
} zend_catch {
642630
do_bailout = true;
643631
} zend_end_try();
644632

633+
if (UNEXPECTED(EG(exception))) {
634+
if (!(coroutine->flags & ZEND_FIBER_FLAG_DESTROYED)
635+
|| !(zend_is_graceful_exit(EG(exception)) || zend_is_unwind_exit(EG(exception)))
636+
) {
637+
coroutine->flags |= ZEND_FIBER_FLAG_THREW;
638+
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_ERROR;
639+
640+
ZVAL_OBJ_COPY(&transfer->value, EG(exception));
641+
}
642+
643+
zend_clear_exception();
644+
}
645+
645646
if (EXPECTED(ZEND_ASYNC_SCHEDULER != &coroutine->coroutine)) {
646647
// Permanently remove the coroutine from the Scheduler.
647648
if (UNEXPECTED(zend_hash_index_del(&ASYNC_G(coroutines), coroutine->std.handle) == FAILURE)) {
648649
zend_error(E_CORE_ERROR, "Failed to remove coroutine from the list");
649650
}
650651

651-
// Decrease the active coroutine count if the coroutine is not a zombie and is started.
652-
if (ZEND_COROUTINE_IS_STARTED(&coroutine->coroutine)
653-
&& false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine)) {
652+
// Decrease the active coroutine count if the coroutine is not a zombie.
653+
if (false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine)) {
654654
ZEND_ASYNC_DECREASE_COROUTINE_COUNT
655655
}
656656
}
@@ -731,6 +731,7 @@ ZEND_STACK_ALIGNED void async_coroutine_execute(zend_fiber_transfer *transfer)
731731
}
732732

733733
EG(vm_stack) = NULL;
734+
bool should_start_graceful_shutdown = false;
734735

735736
zend_first_try {
736737
zend_vm_stack stack = zend_vm_stack_new_page(ZEND_FIBER_VM_STACK_SIZE, NULL);
@@ -767,16 +768,31 @@ ZEND_STACK_ALIGNED void async_coroutine_execute(zend_fiber_transfer *transfer)
767768
coroutine->coroutine.internal_entry();
768769
}
769770

770-
async_coroutine_finalize(transfer, coroutine);
771+
} zend_catch {
772+
coroutine->flags |= ZEND_FIBER_FLAG_BAILOUT;
773+
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_BAILOUT;
774+
should_start_graceful_shutdown = true;
775+
} zend_end_try();
771776

777+
zend_first_try {
778+
async_coroutine_finalize(transfer, coroutine);
772779
} zend_catch {
773780
coroutine->flags |= ZEND_FIBER_FLAG_BAILOUT;
774781
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_BAILOUT;
782+
should_start_graceful_shutdown = true;
775783
} zend_end_try();
776784

777785
coroutine->context.cleanup = &async_coroutine_cleanup;
778786
coroutine->vm_stack = EG(vm_stack);
779787

788+
if (UNEXPECTED(should_start_graceful_shutdown)) {
789+
zend_first_try {
790+
ZEND_ASYNC_SHUTDOWN();
791+
} zend_catch {
792+
zend_error(E_CORE_WARNING, "A critical error was detected during the initiation of the graceful shutdown mode.");
793+
} zend_end_try();
794+
}
795+
780796
//
781797
// The scheduler coroutine always terminates into the main execution flow.
782798
//
@@ -787,7 +803,9 @@ ZEND_STACK_ALIGNED void async_coroutine_execute(zend_fiber_transfer *transfer)
787803
if (transfer != ASYNC_G(main_transfer)) {
788804

789805
if (UNEXPECTED(Z_TYPE(transfer->value) == IS_OBJECT)) {
790-
zval_ptr_dtor(&transfer->value);
806+
zend_first_try {
807+
zval_ptr_dtor(&transfer->value);
808+
} zend_end_try();
791809
zend_error(E_CORE_WARNING, "The transfer value must be NULL when the main coroutine is resumed");
792810
}
793811

exceptions.c

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,15 @@ ZEND_API ZEND_COLD zend_object * async_throw_error(const char *format, ...)
104104
zend_string *message = zend_vstrpprintf(0, format, args);
105105
va_end(args);
106106

107-
zend_object *obj = zend_throw_exception(async_ce_async_exception, ZSTR_VAL(message), 0);
107+
zend_object *obj = NULL;
108+
109+
if (EXPECTED(EG(current_execute_data))) {
110+
obj = zend_throw_exception(async_ce_async_exception, ZSTR_VAL(message), 0);
111+
} else {
112+
obj = async_new_exception(async_ce_async_exception, ZSTR_VAL(message));
113+
async_apply_exception_to_context(obj);
114+
}
115+
108116
zend_string_release(message);
109117
return obj;
110118
}
@@ -124,7 +132,14 @@ ZEND_API ZEND_COLD zend_object * async_throw_cancellation(const char *format, ..
124132
va_list args;
125133
va_start(args, format);
126134

127-
zend_object *obj = zend_throw_exception_ex(async_ce_cancellation_exception, 0, format, args);
135+
zend_object *obj = NULL;
136+
137+
if (EXPECTED(EG(current_execute_data))) {
138+
obj = zend_throw_exception_ex(async_ce_cancellation_exception, 0, format, args);
139+
} else {
140+
obj = async_new_exception(async_ce_cancellation_exception, format, args);
141+
async_apply_exception_to_context(obj);
142+
}
128143

129144
va_end(args);
130145
return obj;
@@ -137,7 +152,14 @@ ZEND_API ZEND_COLD zend_object * async_throw_input_output(const char *format, ..
137152
va_list args;
138153
va_start(args, format);
139154

140-
zend_object *obj = zend_throw_exception_ex(async_ce_input_output_exception, 0, format, args);
155+
zend_object *obj = NULL;
156+
157+
if (EXPECTED(EG(current_execute_data))) {
158+
obj = zend_throw_exception_ex(async_ce_input_output_exception, 0, format, args);
159+
} else {
160+
obj = async_new_exception(async_ce_input_output_exception, format, args);
161+
async_apply_exception_to_context(obj);
162+
}
141163

142164
va_end(args);
143165
return obj;
@@ -147,15 +169,28 @@ ZEND_API ZEND_COLD zend_object * async_throw_timeout(const char *format, const z
147169
{
148170
format = format ? format : "A timeout of %u microseconds occurred";
149171

150-
return zend_throw_exception_ex(async_ce_timeout_exception, 0, format, timeout);
172+
if (EXPECTED(EG(current_execute_data))) {
173+
return zend_throw_exception_ex(async_ce_timeout_exception, 0, format, timeout);
174+
} else {
175+
zend_object *obj = async_new_exception(async_ce_timeout_exception, format, timeout);
176+
async_apply_exception_to_context(obj);
177+
return obj;
178+
}
151179
}
152180

153181
ZEND_API ZEND_COLD zend_object * async_throw_poll(const char *format, ...)
154182
{
155183
va_list args;
156184
va_start(args, format);
157185

158-
zend_object *obj = zend_throw_exception_ex(async_ce_poll_exception, 0, format, args);
186+
zend_object *obj = NULL;
187+
188+
if (EXPECTED(EG(current_execute_data))) {
189+
obj = zend_throw_exception_ex(async_ce_poll_exception, 0, format, args);
190+
} else {
191+
obj = async_new_exception(async_ce_poll_exception, format, args);
192+
async_apply_exception_to_context(obj);
193+
}
159194

160195
va_end(args);
161196
return obj;

scheduler.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ void start_graceful_shutdown(void)
458458

459459
// If the exit exception is not defined, we will define it.
460460
if (EG(exception) == NULL && ZEND_ASYNC_EXIT_EXCEPTION == NULL) {
461-
async_throw_error("Graceful shutdown mode is activated");
461+
zend_error(E_CORE_WARNING, "Graceful shutdown mode was started");
462462
}
463463

464464
if (EG(exception) != NULL) {
@@ -676,7 +676,7 @@ void async_scheduler_main_coroutine_suspend(void)
676676
} zend_end_try();
677677

678678
ZEND_ASYNC_CURRENT_COROUTINE = NULL;
679-
ZEND_ASSERT(ZEND_ASYNC_ACTIVE_COROUTINE_COUNT == 0 && "The active coroutine counter must be 1 at this point");
679+
ZEND_ASSERT(ZEND_ASYNC_ACTIVE_COROUTINE_COUNT == 0 && "The active coroutine counter must be 0 at this point");
680680
ZEND_ASYNC_DEACTIVATE;
681681

682682
if (ASYNC_G(main_transfer)) {

scope.c

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1412,12 +1412,29 @@ static zend_always_inline bool try_to_handle_exception(
14121412
}
14131413
}
14141414

1415+
async_scope_object_t *scope_object = NULL;
1416+
1417+
if (UNEXPECTED(current_scope->scope.scope_object == NULL)) {
1418+
// The PHP Scope object might already be destroyed by the time the internal Scope still exists.
1419+
// To normalize this situation, we’ll create a fake Scope object that will serve as a bridge.
1420+
scope_object = ZEND_OBJECT_ALLOC_EX(sizeof(async_scope_object_t), async_ce_scope);
1421+
zend_object_std_init(&scope_object->std, async_ce_scope);
1422+
object_properties_init(&scope_object->std, async_ce_scope);
1423+
1424+
if (UNEXPECTED(EG(exception))) {
1425+
OBJ_RELEASE(&scope_object->std);
1426+
return false;
1427+
}
1428+
1429+
GC_ADDREF(&scope_object->std);
1430+
scope_object->scope = current_scope;
1431+
}
14151432

14161433
// Prototype: function (Async\Scope $scope, Async\Coroutine $coroutine, Throwable $e)
14171434
zval retval;
14181435
zval parameters[3];
14191436
ZVAL_UNDEF(&retval);
1420-
ZVAL_OBJ(&parameters[0], current_scope->scope.scope_object);
1437+
ZVAL_OBJ(&parameters[0], scope_object != NULL ? &scope_object->std : current_scope->scope.scope_object);
14211438
ZVAL_OBJ(&parameters[1], &coroutine->std);
14221439
ZVAL_OBJ(&parameters[2], exception);
14231440

@@ -1438,6 +1455,14 @@ static zend_always_inline bool try_to_handle_exception(
14381455
exception_fci->params = NULL;
14391456

14401457
if (result == SUCCESS && EG(exception) == NULL) {
1458+
if (UNEXPECTED(scope_object != NULL)) {
1459+
scope_object->scope = NULL; // Clear reference to avoid double release
1460+
if (GC_REFCOUNT(&scope_object->std) > 1) {
1461+
GC_DELREF(&scope_object->std);
1462+
}
1463+
OBJ_RELEASE(&scope_object->std);
1464+
}
1465+
14411466
return true;
14421467
}
14431468
}
@@ -1458,10 +1483,26 @@ static zend_always_inline bool try_to_handle_exception(
14581483
exception_fci->params = NULL;
14591484

14601485
if (result == SUCCESS && EG(exception) == NULL) {
1486+
if (UNEXPECTED(scope_object != NULL)) {
1487+
scope_object->scope = NULL;
1488+
if (GC_REFCOUNT(&scope_object->std) > 1) {
1489+
GC_DELREF(&scope_object->std);
1490+
}
1491+
OBJ_RELEASE(&scope_object->std);
1492+
}
1493+
14611494
return true;
14621495
}
14631496
}
14641497

1498+
if (UNEXPECTED(scope_object != NULL)) {
1499+
scope_object->scope = NULL;
1500+
if (GC_REFCOUNT(&scope_object->std) > 1) {
1501+
GC_DELREF(&scope_object->std);
1502+
}
1503+
OBJ_RELEASE(&scope_object->std);
1504+
}
1505+
14651506
return false; // Exception not handled
14661507
}
14671508

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Memory exhaustion bailout in simple async operation
3+
--SKIPIF--
4+
<?php
5+
$zend_mm_enabled = getenv("USE_ZEND_ALLOC");
6+
if ($zend_mm_enabled === "0") {
7+
die("skip Zend MM disabled");
8+
}
9+
?>
10+
--INI--
11+
memory_limit=2M
12+
--FILE--
13+
<?php
14+
15+
use function Async\spawn;
16+
17+
register_shutdown_function(function() {
18+
echo "Shutdown function called\n";
19+
});
20+
21+
echo "Before spawn\n";
22+
23+
spawn(function() {
24+
echo "Before memory exhaustion\n";
25+
str_repeat('x', 10000000);
26+
echo "After memory exhaustion (should not reach)\n";
27+
});
28+
29+
echo "After spawn\n";
30+
31+
?>
32+
--EXPECTF--
33+
Before spawn
34+
After spawn
35+
Before memory exhaustion
36+
37+
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
38+
39+
Warning: Graceful shutdown mode was started in %s on line %d
40+
Shutdown function called
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
Memory exhaustion bailout in nested async operations
3+
--SKIPIF--
4+
<?php
5+
$zend_mm_enabled = getenv("USE_ZEND_ALLOC");
6+
if ($zend_mm_enabled === "0") {
7+
die("skip Zend MM disabled");
8+
}
9+
?>
10+
--INI--
11+
memory_limit=2M
12+
--FILE--
13+
<?php
14+
15+
use function Async\spawn;
16+
17+
register_shutdown_function(function() {
18+
echo "Shutdown function called\n";
19+
});
20+
21+
echo "Before spawn\n";
22+
23+
spawn(function() {
24+
echo "Outer async started\n";
25+
26+
spawn(function() {
27+
echo "Inner async started\n";
28+
str_repeat('x', 10000000);
29+
echo "Inner async after memory exhaustion (should not reach)\n";
30+
});
31+
32+
echo "Outer async continues\n";
33+
});
34+
35+
echo "After spawn\n";
36+
37+
?>
38+
--EXPECTF--
39+
Before spawn
40+
After spawn
41+
Outer async started
42+
Outer async continues
43+
Inner async started
44+
45+
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
46+
47+
Warning: Graceful shutdown mode was started in %s on line %d
48+
Shutdown function called

0 commit comments

Comments
 (0)