Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
2 changes: 2 additions & 0 deletions async_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
22 changes: 22 additions & 0 deletions exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions exceptions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion exceptions_arginfo.h
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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;
Expand Down
47 changes: 23 additions & 24 deletions scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 4 additions & 5 deletions tests/edge_cases/001-deadlock-basic-test.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 6 additions & 7 deletions tests/edge_cases/002-deadlock-with-catch.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
13 changes: 6 additions & 7 deletions tests/edge_cases/003-deadlock-with-zombie.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
13 changes: 6 additions & 7 deletions tests/edge_cases/010-deadlock-after-cancel-with-zombie.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Loading