Skip to content

Commit 7c7d5a6

Browse files
committed
#64: Refactor deadlock detection to use structured exceptions instead of warnings
- Add new Async\DeadlockError exception class extending Error - Replace multiple async_warning() calls with single structured exception - Implement proper exit_exception handling for inter-coroutine safety - Update resolve_deadlocks() to set ZEND_ASYNC_EXIT_EXCEPTION correctly - Add ZEND_ASYNC_EXCEPTION_DEADLOCK to async class enum (ID: 36) - Extend async_get_class_ce() to support new exception type - Update all deadlock tests to expect exception instead of warnings - Fix memory leak by removing unnecessary GC_ADDREF calls This change improves error handling by providing a single, catchable DeadlockError exception instead of multiple warnings, while maintaining proper coroutine cancellation behavior through exit_exception mechanism. Tests updated: - 001-deadlock-basic-test.phpt - 002-deadlock-with-catch.phpt - 003-deadlock-with-zombie.phpt - 010-deadlock-after-cancel-with-zombie.phpt
1 parent da80a88 commit 7c7d5a6

11 files changed

+96
-52
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ All notable changes to the Async extension for PHP will be documented in this fi
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.4.0] - 2025-09-31
8+
## [0.5.0] - 2025-10-31
9+
10+
### Changed
11+
- **Deadlock Detection**: Replaced warnings with structured exception handling
12+
- Deadlock detection now throws `Async\DeadlockError` exception instead of multiple warnings
13+
- **Breaking Change**: Applications relying on deadlock warnings
14+
will need to be updated to catch `Async\DeadlockError` exceptions
15+
16+
## [0.4.0] - 2025-09-30
917

1018
### Added
1119
- **UDP socket stream support for TrueAsync**

async_API.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ static zend_class_entry *async_get_class_ce(zend_async_class type)
267267
return async_ce_poll_exception;
268268
case ZEND_ASYNC_EXCEPTION_DNS:
269269
return async_ce_dns_exception;
270+
case ZEND_ASYNC_EXCEPTION_DEADLOCK:
271+
return async_ce_deadlock_error;
270272
default:
271273
return NULL;
272274
}

exceptions.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ zend_class_entry *async_ce_input_output_exception = NULL;
2929
zend_class_entry *async_ce_timeout_exception = NULL;
3030
zend_class_entry *async_ce_poll_exception = NULL;
3131
zend_class_entry *async_ce_dns_exception = NULL;
32+
zend_class_entry *async_ce_deadlock_error = NULL;
3233
zend_class_entry *async_ce_composite_exception = NULL;
3334

3435
PHP_METHOD(Async_CompositeException, addException)
@@ -66,6 +67,7 @@ void async_register_exceptions_ce(void)
6667
async_ce_timeout_exception = register_class_Async_TimeoutException(zend_ce_exception);
6768
async_ce_poll_exception = register_class_Async_PollException(zend_ce_exception);
6869
async_ce_dns_exception = register_class_Async_DnsException(zend_ce_exception);
70+
async_ce_deadlock_error = register_class_Async_DeadlockError(zend_ce_error);
6971
async_ce_composite_exception = register_class_Async_CompositeException(zend_ce_exception);
7072
}
7173

@@ -194,6 +196,26 @@ PHP_ASYNC_API ZEND_COLD zend_object *async_throw_poll(const char *format, ...)
194196
return obj;
195197
}
196198

199+
PHP_ASYNC_API ZEND_COLD zend_object *async_throw_deadlock(const char *format, ...)
200+
{
201+
format = format ? format : "A deadlock was detected";
202+
203+
va_list args;
204+
va_start(args, format);
205+
206+
zend_object *obj = NULL;
207+
208+
if (EXPECTED(EG(current_execute_data))) {
209+
obj = zend_throw_exception_ex(async_ce_deadlock_error, 0, format, args);
210+
} else {
211+
obj = async_new_exception(async_ce_deadlock_error, format, args);
212+
async_apply_exception_to_context(obj);
213+
}
214+
215+
va_end(args);
216+
return obj;
217+
}
218+
197219
PHP_ASYNC_API ZEND_COLD zend_object *async_new_composite_exception(void)
198220
{
199221
zval composite;

exceptions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ PHP_ASYNC_API extern zend_class_entry *async_ce_input_output_exception;
2929
PHP_ASYNC_API extern zend_class_entry *async_ce_timeout_exception;
3030
PHP_ASYNC_API extern zend_class_entry *async_ce_poll_exception;
3131
PHP_ASYNC_API extern zend_class_entry *async_ce_dns_exception;
32+
PHP_ASYNC_API extern zend_class_entry *async_ce_deadlock_error;
3233
PHP_ASYNC_API extern zend_class_entry *async_ce_composite_exception;
3334

3435
void async_register_exceptions_ce(void);
@@ -38,6 +39,7 @@ PHP_ASYNC_API ZEND_COLD zend_object *async_throw_cancellation(const char *format
3839
PHP_ASYNC_API ZEND_COLD zend_object *async_throw_input_output(const char *format, ...);
3940
PHP_ASYNC_API ZEND_COLD zend_object *async_throw_timeout(const char *format, const zend_long timeout);
4041
PHP_ASYNC_API ZEND_COLD zend_object *async_throw_poll(const char *format, ...);
42+
PHP_ASYNC_API ZEND_COLD zend_object *async_throw_deadlock(const char *format, ...);
4143
PHP_ASYNC_API ZEND_COLD zend_object *async_new_composite_exception(void);
4244
PHP_ASYNC_API void
4345
async_composite_exception_add_exception(zend_object *composite, zend_object *exception, bool transfer);

exceptions.stub.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ class TimeoutException extends \Exception {}
3636
*/
3737
class PollException extends \Exception {}
3838

39+
/**
40+
* Exception thrown when a deadlock is detected.
41+
*/
42+
class DeadlockError extends \Error {}
43+
3944
/**
4045
* Exception that can contain multiple exceptions.
4146
* Used when multiple exceptions occur in finally handlers.

exceptions_arginfo.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 6ee15183630fa16055647deb278f0222bf5db317 */
2+
* Stub hash: f06ce54277b0830aebcbd112c67238db6dca4d9b */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_CompositeException_addException, 0, 1, IS_VOID, 0)
55
ZEND_ARG_OBJ_INFO(0, exception, Throwable, 0)
@@ -77,6 +77,16 @@ static zend_class_entry *register_class_Async_PollException(zend_class_entry *cl
7777
return class_entry;
7878
}
7979

80+
static zend_class_entry *register_class_Async_DeadlockError(zend_class_entry *class_entry_Error)
81+
{
82+
zend_class_entry ce, *class_entry;
83+
84+
INIT_NS_CLASS_ENTRY(ce, "Async", "DeadlockError", NULL);
85+
class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Error, 0);
86+
87+
return class_entry;
88+
}
89+
8090
static zend_class_entry *register_class_Async_CompositeException(zend_class_entry *class_entry_Exception)
8191
{
8292
zend_class_entry ce, *class_entry;

scheduler.c

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -526,37 +526,36 @@ static bool resolve_deadlocks(void)
526526
return false;
527527
}
528528

529-
async_warning("no active coroutines, deadlock detected. Coroutines in waiting: %u", real_coroutines);
529+
// Create deadlock exception to be set as exit_exception
530+
zend_object *deadlock_exception = async_new_exception(async_ce_deadlock_error,
531+
"Deadlock detected: no active coroutines, %u coroutines in waiting", real_coroutines);
530532

531-
ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value)
532-
533-
async_coroutine_t *coroutine = (async_coroutine_t *) Z_PTR_P(value);
534-
535-
ZEND_ASSERT(coroutine->coroutine.waker != NULL && "The Coroutine has no waker object");
536-
537-
if (coroutine->coroutine.waker != NULL && coroutine->coroutine.waker->filename != NULL) {
533+
// Set as exit exception if there isn't one already
534+
if (ZEND_ASYNC_EXIT_EXCEPTION == NULL) {
535+
ZEND_ASYNC_EXIT_EXCEPTION = deadlock_exception;
536+
} else {
537+
// If there's already an exit exception, make the deadlock exception previous
538+
zend_exception_set_previous(deadlock_exception, ZEND_ASYNC_EXIT_EXCEPTION);
539+
ZEND_ASYNC_EXIT_EXCEPTION = deadlock_exception;
540+
}
538541

539-
// Maybe we need to get the function name
540-
// zend_string * function_name = NULL;
541-
// zend_get_function_name_by_fci(&fiber_state->fiber->fci, &fiber_state->fiber->fci_cache, &function_name);
542+
ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) {
543+
async_coroutine_t *coroutine = (async_coroutine_t *) Z_PTR_P(value);
542544

543-
async_warning("the coroutine was suspended in file: %s, line: %d will be canceled",
544-
ZSTR_VAL(coroutine->coroutine.waker->filename),
545-
coroutine->coroutine.waker->lineno);
546-
}
545+
ZEND_ASSERT(coroutine->coroutine.waker != NULL && "The Coroutine has no waker object");
547546

548-
// In case a deadlock condition is detected, cancellation protection flags no longer apply.
549-
if (ZEND_COROUTINE_IS_PROTECTED(&coroutine->coroutine)) {
550-
ZEND_COROUTINE_CLR_PROTECTED(&coroutine->coroutine);
551-
}
547+
// In case a deadlock condition is detected, cancellation protection flags no longer apply.
548+
if (ZEND_COROUTINE_IS_PROTECTED(&coroutine->coroutine)) {
549+
ZEND_COROUTINE_CLR_PROTECTED(&coroutine->coroutine);
550+
}
552551

553-
ZEND_ASYNC_CANCEL(
554-
&coroutine->coroutine, async_new_exception(async_ce_cancellation_exception, "Deadlock detected"), true);
552+
ZEND_ASYNC_CANCEL(
553+
&coroutine->coroutine, async_new_exception(async_ce_cancellation_exception, "Deadlock detected"), true);
555554

556-
if (EG(exception) != NULL) {
557-
return true;
555+
if (EG(exception) != NULL) {
556+
return true;
557+
}
558558
}
559-
560559
ZEND_HASH_FOREACH_END();
561560

562561
return false;

tests/edge_cases/001-deadlock-basic-test.phpt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ end
3434
coroutine1 running
3535
coroutine2 running
3636

37-
Warning: no active coroutines, deadlock detected. Coroutines in waiting: %d in Unknown on line %d
38-
39-
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d
40-
41-
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d
37+
Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0
38+
Stack trace:
39+
#0 {main}
40+
thrown in [no active file] on line 0

tests/edge_cases/002-deadlock-with-catch.phpt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,12 @@ start
4141
end
4242
coroutine1 running
4343
coroutine2 running
44-
45-
Warning: no active coroutines, deadlock detected. Coroutines in waiting: %d in Unknown on line %d
46-
47-
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d
48-
49-
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d
5044
Caught exception: Deadlock detected
5145
coroutine1 finished
5246
Caught exception: Deadlock detected
53-
coroutine2 finished
47+
coroutine2 finished
48+
49+
Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0
50+
Stack trace:
51+
#0 {main}
52+
thrown in [no active file] on line 0

tests/edge_cases/003-deadlock-with-zombie.phpt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,12 @@ start
4747
end
4848
coroutine1 running
4949
coroutine2 running
50-
51-
Warning: no active coroutines, deadlock detected. Coroutines in waiting: %d in Unknown on line %d
52-
53-
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d
54-
55-
Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d
5650
Caught exception: Deadlock detected
5751
Caught exception: Deadlock detected
5852
coroutine1 finished
59-
coroutine2 finished
53+
coroutine2 finished
54+
55+
Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0
56+
Stack trace:
57+
#0 {main}
58+
thrown in [no active file] on line 0

0 commit comments

Comments
 (0)