diff --git a/CHANGELOG.md b/CHANGELOG.md index c25745e..484d52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,15 @@ All notable changes to the Async extension for PHP will be documented in this fi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.4.0] - 2025-09-31 +## [0.5.0] - 2025-10-31 + +### Changed +- **Deadlock Detection**: Replaced warnings with structured exception handling + - Deadlock detection now throws `Async\DeadlockError` exception instead of multiple warnings + - **Breaking Change**: Applications relying on deadlock warnings + will need to be updated to catch `Async\DeadlockError` exceptions + +## [0.4.0] - 2025-09-30 ### Added - **UDP socket stream support for TrueAsync** diff --git a/async_API.c b/async_API.c index 2c4ef4a..142f41a 100644 --- a/async_API.c +++ b/async_API.c @@ -267,6 +267,8 @@ static zend_class_entry *async_get_class_ce(zend_async_class type) return async_ce_poll_exception; case ZEND_ASYNC_EXCEPTION_DNS: return async_ce_dns_exception; + case ZEND_ASYNC_EXCEPTION_DEADLOCK: + return async_ce_deadlock_error; default: return NULL; } diff --git a/exceptions.c b/exceptions.c index b326989..4660971 100644 --- a/exceptions.c +++ b/exceptions.c @@ -29,6 +29,7 @@ zend_class_entry *async_ce_input_output_exception = NULL; zend_class_entry *async_ce_timeout_exception = NULL; zend_class_entry *async_ce_poll_exception = NULL; zend_class_entry *async_ce_dns_exception = NULL; +zend_class_entry *async_ce_deadlock_error = NULL; zend_class_entry *async_ce_composite_exception = NULL; PHP_METHOD(Async_CompositeException, addException) @@ -66,6 +67,7 @@ void async_register_exceptions_ce(void) async_ce_timeout_exception = register_class_Async_TimeoutException(zend_ce_exception); async_ce_poll_exception = register_class_Async_PollException(zend_ce_exception); async_ce_dns_exception = register_class_Async_DnsException(zend_ce_exception); + async_ce_deadlock_error = register_class_Async_DeadlockError(zend_ce_error); async_ce_composite_exception = register_class_Async_CompositeException(zend_ce_exception); } @@ -194,6 +196,26 @@ PHP_ASYNC_API ZEND_COLD zend_object *async_throw_poll(const char *format, ...) return obj; } +PHP_ASYNC_API ZEND_COLD zend_object *async_throw_deadlock(const char *format, ...) +{ + format = format ? format : "A deadlock was detected"; + + va_list args; + va_start(args, format); + + zend_object *obj = NULL; + + if (EXPECTED(EG(current_execute_data))) { + obj = zend_throw_exception_ex(async_ce_deadlock_error, 0, format, args); + } else { + obj = async_new_exception(async_ce_deadlock_error, format, args); + async_apply_exception_to_context(obj); + } + + va_end(args); + return obj; +} + PHP_ASYNC_API ZEND_COLD zend_object *async_new_composite_exception(void) { zval composite; diff --git a/exceptions.h b/exceptions.h index e59b263..7d8590c 100644 --- a/exceptions.h +++ b/exceptions.h @@ -29,6 +29,7 @@ PHP_ASYNC_API extern zend_class_entry *async_ce_input_output_exception; PHP_ASYNC_API extern zend_class_entry *async_ce_timeout_exception; PHP_ASYNC_API extern zend_class_entry *async_ce_poll_exception; PHP_ASYNC_API extern zend_class_entry *async_ce_dns_exception; +PHP_ASYNC_API extern zend_class_entry *async_ce_deadlock_error; PHP_ASYNC_API extern zend_class_entry *async_ce_composite_exception; void async_register_exceptions_ce(void); @@ -38,6 +39,7 @@ PHP_ASYNC_API ZEND_COLD zend_object *async_throw_cancellation(const char *format PHP_ASYNC_API ZEND_COLD zend_object *async_throw_input_output(const char *format, ...); PHP_ASYNC_API ZEND_COLD zend_object *async_throw_timeout(const char *format, const zend_long timeout); PHP_ASYNC_API ZEND_COLD zend_object *async_throw_poll(const char *format, ...); +PHP_ASYNC_API ZEND_COLD zend_object *async_throw_deadlock(const char *format, ...); PHP_ASYNC_API ZEND_COLD zend_object *async_new_composite_exception(void); PHP_ASYNC_API void async_composite_exception_add_exception(zend_object *composite, zend_object *exception, bool transfer); diff --git a/exceptions.stub.php b/exceptions.stub.php index 5899d8c..48dfd8d 100644 --- a/exceptions.stub.php +++ b/exceptions.stub.php @@ -36,6 +36,11 @@ class TimeoutException extends \Exception {} */ class PollException extends \Exception {} +/** + * Exception thrown when a deadlock is detected. + */ +class DeadlockError extends \Error {} + /** * Exception that can contain multiple exceptions. * Used when multiple exceptions occur in finally handlers. diff --git a/exceptions_arginfo.h b/exceptions_arginfo.h index a065739..af86dc9 100644 --- a/exceptions_arginfo.h +++ b/exceptions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 6ee15183630fa16055647deb278f0222bf5db317 */ + * Stub hash: f06ce54277b0830aebcbd112c67238db6dca4d9b */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_CompositeException_addException, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, exception, Throwable, 0) @@ -77,6 +77,16 @@ static zend_class_entry *register_class_Async_PollException(zend_class_entry *cl return class_entry; } +static zend_class_entry *register_class_Async_DeadlockError(zend_class_entry *class_entry_Error) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Async", "DeadlockError", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Error, 0); + + return class_entry; +} + static zend_class_entry *register_class_Async_CompositeException(zend_class_entry *class_entry_Exception) { zend_class_entry ce, *class_entry; diff --git a/scheduler.c b/scheduler.c index b108770..2d171f7 100644 --- a/scheduler.c +++ b/scheduler.c @@ -526,37 +526,36 @@ static bool resolve_deadlocks(void) return false; } - async_warning("no active coroutines, deadlock detected. Coroutines in waiting: %u", real_coroutines); + // Create deadlock exception to be set as exit_exception + zend_object *deadlock_exception = async_new_exception(async_ce_deadlock_error, + "Deadlock detected: no active coroutines, %u coroutines in waiting", real_coroutines); - ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) - - async_coroutine_t *coroutine = (async_coroutine_t *) Z_PTR_P(value); - - ZEND_ASSERT(coroutine->coroutine.waker != NULL && "The Coroutine has no waker object"); - - if (coroutine->coroutine.waker != NULL && coroutine->coroutine.waker->filename != NULL) { + // Set as exit exception if there isn't one already + if (ZEND_ASYNC_EXIT_EXCEPTION == NULL) { + ZEND_ASYNC_EXIT_EXCEPTION = deadlock_exception; + } else { + // If there's already an exit exception, make the deadlock exception previous + zend_exception_set_previous(deadlock_exception, ZEND_ASYNC_EXIT_EXCEPTION); + ZEND_ASYNC_EXIT_EXCEPTION = deadlock_exception; + } - // Maybe we need to get the function name - // zend_string * function_name = NULL; - // zend_get_function_name_by_fci(&fiber_state->fiber->fci, &fiber_state->fiber->fci_cache, &function_name); + ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) { + async_coroutine_t *coroutine = (async_coroutine_t *) Z_PTR_P(value); - async_warning("the coroutine was suspended in file: %s, line: %d will be canceled", - ZSTR_VAL(coroutine->coroutine.waker->filename), - coroutine->coroutine.waker->lineno); - } + ZEND_ASSERT(coroutine->coroutine.waker != NULL && "The Coroutine has no waker object"); - // In case a deadlock condition is detected, cancellation protection flags no longer apply. - if (ZEND_COROUTINE_IS_PROTECTED(&coroutine->coroutine)) { - ZEND_COROUTINE_CLR_PROTECTED(&coroutine->coroutine); - } + // In case a deadlock condition is detected, cancellation protection flags no longer apply. + if (ZEND_COROUTINE_IS_PROTECTED(&coroutine->coroutine)) { + ZEND_COROUTINE_CLR_PROTECTED(&coroutine->coroutine); + } - ZEND_ASYNC_CANCEL( - &coroutine->coroutine, async_new_exception(async_ce_cancellation_exception, "Deadlock detected"), true); + ZEND_ASYNC_CANCEL( + &coroutine->coroutine, async_new_exception(async_ce_cancellation_exception, "Deadlock detected"), true); - if (EG(exception) != NULL) { - return true; + if (EG(exception) != NULL) { + return true; + } } - ZEND_HASH_FOREACH_END(); return false; diff --git a/tests/edge_cases/001-deadlock-basic-test.phpt b/tests/edge_cases/001-deadlock-basic-test.phpt index 1a9b63f..e0c87bf 100644 --- a/tests/edge_cases/001-deadlock-basic-test.phpt +++ b/tests/edge_cases/001-deadlock-basic-test.phpt @@ -34,8 +34,7 @@ end coroutine1 running coroutine2 running -Warning: no active coroutines, deadlock detected. Coroutines in waiting: %d in Unknown on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d +Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0 +Stack trace: +#0 {main} + thrown in [no active file] on line 0 diff --git a/tests/edge_cases/002-deadlock-with-catch.phpt b/tests/edge_cases/002-deadlock-with-catch.phpt index 8dbaf79..42b2441 100644 --- a/tests/edge_cases/002-deadlock-with-catch.phpt +++ b/tests/edge_cases/002-deadlock-with-catch.phpt @@ -41,13 +41,12 @@ start end coroutine1 running coroutine2 running - -Warning: no active coroutines, deadlock detected. Coroutines in waiting: %d in Unknown on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d Caught exception: Deadlock detected coroutine1 finished Caught exception: Deadlock detected -coroutine2 finished \ No newline at end of file +coroutine2 finished + +Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0 +Stack trace: +#0 {main} + thrown in [no active file] on line 0 \ No newline at end of file diff --git a/tests/edge_cases/003-deadlock-with-zombie.phpt b/tests/edge_cases/003-deadlock-with-zombie.phpt index 0309800..b618a55 100644 --- a/tests/edge_cases/003-deadlock-with-zombie.phpt +++ b/tests/edge_cases/003-deadlock-with-zombie.phpt @@ -47,13 +47,12 @@ start end coroutine1 running coroutine2 running - -Warning: no active coroutines, deadlock detected. Coroutines in waiting: %d in Unknown on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in Unknown on line %d Caught exception: Deadlock detected Caught exception: Deadlock detected coroutine1 finished -coroutine2 finished \ No newline at end of file +coroutine2 finished + +Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0 +Stack trace: +#0 {main} + thrown in [no active file] on line 0 \ No newline at end of file diff --git a/tests/edge_cases/010-deadlock-after-cancel-with-zombie.phpt b/tests/edge_cases/010-deadlock-after-cancel-with-zombie.phpt index 1f589ee..a4fe6bd 100644 --- a/tests/edge_cases/010-deadlock-after-cancel-with-zombie.phpt +++ b/tests/edge_cases/010-deadlock-after-cancel-with-zombie.phpt @@ -54,13 +54,12 @@ start coroutine1 running coroutine2 running end - -Warning: no active coroutines, deadlock detected. Coroutines in waiting: 2 in %s on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in %s on line %d - -Warning: the coroutine was suspended in file: %s, line: %d will be canceled in %s on line %d Caught exception: Deadlock detected Caught exception: Deadlock detected coroutine1 finished -coroutine2 finished \ No newline at end of file +coroutine2 finished + +Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0 +Stack trace: +#0 {main} + thrown in [no active file] on line 0 \ No newline at end of file