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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimized `network_async_accept_incoming` to try `accept()` before waiting
- Enhanced stream_select functionality with event-driven architecture
- Improved blocking operation handling with boolean return values
- **TrueAsync API Performance**: Optimized execution paths by replacing expensive `EG(exception)` checks with direct `bool` return values across all async functions
- Upgrade `LibUV` to version `1.45` due to a timer bug that causes the application to hang

## [0.3.0] - 2025-07-16
Expand Down
9 changes: 3 additions & 6 deletions async.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ static zend_object *async_timeout_create(zend_ulong ms, bool is_periodic);

#define SCHEDULER_LAUNCH \
if (UNEXPECTED(ZEND_ASYNC_CURRENT_COROUTINE == NULL)) { \
async_scheduler_launch(); \
if (UNEXPECTED(EG(exception) != NULL)) { \
if (!async_scheduler_launch()) { \
RETURN_THROWS(); \
} \
}
Expand All @@ -79,7 +78,7 @@ PHP_FUNCTION(Async_spawn)

async_coroutine_t *coroutine = (async_coroutine_t *) ZEND_ASYNC_SPAWN();

if (UNEXPECTED(EG(exception))) {
if (UNEXPECTED(coroutine == NULL)) {
return;
}

Expand Down Expand Up @@ -275,9 +274,7 @@ PHP_FUNCTION(Async_await)
}
}

ZEND_ASYNC_SUSPEND();

if (UNEXPECTED(EG(exception) != NULL)) {
if (!ZEND_ASYNC_SUSPEND()) {
zend_async_waker_clean(coroutine);
RETURN_THROWS();
}
Expand Down
49 changes: 27 additions & 22 deletions async_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,34 +58,34 @@ zend_coroutine_t *spawn(zend_async_scope_t *scope, zend_object *scope_provider,
async_throw_error("Cannot spawn a coroutine when async is disabled");
return NULL;
} else if (UNEXPECTED(ZEND_ASYNC_IS_READY)) {
async_scheduler_launch();
if (UNEXPECTED(EG(exception) != NULL)) {
if (UNEXPECTED(!async_scheduler_launch())) {
return NULL;
}
}

if (scope == NULL && scope_provider != NULL) {
scope = async_provide_scope(scope_provider);

if (UNEXPECTED(EG(exception) != NULL)) {
if (UNEXPECTED(EG(exception))) {
return NULL;
}
}

if (scope == NULL) {

if (UNEXPECTED(ZEND_ASYNC_CURRENT_SCOPE == NULL && ZEND_ASYNC_MAIN_SCOPE == NULL)) {
ZEND_ASYNC_MAIN_SCOPE = ZEND_ASYNC_NEW_SCOPE(NULL);
zend_async_scope_t **main_scope_ptr = &ZEND_ASYNC_MAIN_SCOPE;

if (UNEXPECTED(EG(exception))) {
if (UNEXPECTED(ZEND_ASYNC_CURRENT_SCOPE == NULL && *main_scope_ptr == NULL)) {
*main_scope_ptr = ZEND_ASYNC_NEW_SCOPE(NULL);

if (UNEXPECTED(*main_scope_ptr == NULL)) {
return NULL;
}
}

if (EXPECTED(ZEND_ASYNC_CURRENT_SCOPE != NULL)) {
scope = ZEND_ASYNC_CURRENT_SCOPE;
} else {
scope = ZEND_ASYNC_MAIN_SCOPE;
scope = *main_scope_ptr;
}
}

Expand All @@ -100,21 +100,20 @@ zend_coroutine_t *spawn(zend_async_scope_t *scope, zend_object *scope_provider,
}

async_coroutine_t *coroutine = (async_coroutine_t *) async_new_coroutine(scope);
if (UNEXPECTED(EG(exception))) {
if (UNEXPECTED(coroutine == NULL)) {
return NULL;
}

zend_apply_current_filename_and_line(&coroutine->coroutine.filename, &coroutine->coroutine.lineno);

zval options;
ZVAL_UNDEF(&options);
scope->before_coroutine_enqueue(&coroutine->coroutine, scope, &options);
zval_dtor(&options);

if (UNEXPECTED(EG(exception))) {
if (!scope->before_coroutine_enqueue(&coroutine->coroutine, scope, &options)) {
zval_dtor(&options);
coroutine->coroutine.event.dispose(&coroutine->coroutine.event);
return NULL;
}
zval_dtor(&options);

const bool is_spawn_strategy =
scope_provider != NULL && instanceof_function(scope_provider->ce, async_ce_spawn_strategy);
Expand All @@ -141,7 +140,7 @@ zend_coroutine_t *spawn(zend_async_scope_t *scope, zend_object *scope_provider,
}

zend_async_waker_t *waker = zend_async_waker_new(&coroutine->coroutine);
if (UNEXPECTED(EG(exception))) {
if (UNEXPECTED(waker == NULL)) {
coroutine->coroutine.event.dispose(&coroutine->coroutine.event);
return NULL;
}
Expand Down Expand Up @@ -202,7 +201,7 @@ zend_coroutine_t *spawn(zend_async_scope_t *scope, zend_object *scope_provider,
return &coroutine->coroutine;
}

static void engine_shutdown(void)
static bool engine_shutdown(void)
{
ZEND_ASYNC_REACTOR_SHUTDOWN();

Expand All @@ -218,25 +217,27 @@ static void engine_shutdown(void)
}

// async_host_name_list_dtor();
return true;
}

zend_array *get_coroutines(void)
{
return &ASYNC_G(coroutines);
}

void add_microtask(zend_async_microtask_t *microtask)
bool add_microtask(zend_async_microtask_t *microtask)
{
if (microtask->is_cancelled) {
return;
return true;
}

if (UNEXPECTED(circular_buffer_push(&ASYNC_G(microtasks), &microtask, true) == FAILURE)) {
async_throw_error("Failed to enqueue microtask");
return;
return false;
}

microtask->ref_count++;
return true;
}

zend_array *get_awaiting_info(zend_coroutine_t *coroutine)
Expand Down Expand Up @@ -351,7 +352,9 @@ static void async_waiting_callback(zend_async_event_t *event,
// We remove the callback because we treat all events
// as FUTURE-type objects, where the trigger can be activated only once.
ZEND_ASYNC_EVENT_CALLBACK_ADD_REF(callback);
event->del_callback(event, callback);
if (!event->del_callback(event, callback)) {
// Optimized: ignore del_callback failure in callback context
}
Comment on lines +355 to +357
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Silently ignoring the failure contradicts the purpose of returning boolean status. Consider logging the failure or handling it appropriately, even if it's just a debug trace.

Copilot uses AI. Check for mistakes.
ZEND_ASYNC_EVENT_CALLBACK_DEC_REF(callback);

if (exception != NULL) {
Expand Down Expand Up @@ -446,7 +449,9 @@ static void async_waiting_cancellation_callback(zend_async_event_t *event,

await_context->resolved_count++;
ZEND_ASYNC_EVENT_CALLBACK_ADD_REF(callback);
event->del_callback(event, callback);
if (!event->del_callback(event, callback)) {
// Optimized: ignore del_callback failure in callback context
}
ZEND_ASYNC_EVENT_CALLBACK_DEC_REF(callback);

if (exception != NULL) {
Expand Down Expand Up @@ -1009,15 +1014,15 @@ void async_await_futures(zval *iterable,
// which ensures that all child tasks are stopped if the main task is cancelled.
zend_async_scope_t *scope = ZEND_ASYNC_NEW_SCOPE(ZEND_ASYNC_CURRENT_SCOPE);

if (UNEXPECTED(scope == NULL || EG(exception))) {
if (UNEXPECTED(scope == NULL)) {
zend_iterator_dtor(zend_iterator);
await_context->dtor(await_context);
return;
}

zend_coroutine_t *iterator_coroutine = ZEND_ASYNC_SPAWN_WITH_SCOPE_EX(scope, ZEND_COROUTINE_NORMAL);

if (UNEXPECTED(iterator_coroutine == NULL || EG(exception))) {
if (UNEXPECTED(iterator_coroutine == NULL)) {
zend_iterator_dtor(zend_iterator);
await_context->dtor(await_context);
scope->try_to_dispose(scope);
Expand Down
67 changes: 36 additions & 31 deletions coroutine.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ static void coroutine_call_finally_handlers(async_coroutine_t *coroutine);
static void finally_context_dtor(finally_handlers_context_t *context);

// Forward declarations for event system
static void coroutine_event_start(zend_async_event_t *event);
static void coroutine_event_stop(zend_async_event_t *event);
static void coroutine_add_callback(zend_async_event_t *event, zend_async_event_callback_t *callback);
static void coroutine_del_callback(zend_async_event_t *event, zend_async_event_callback_t *callback);
static bool coroutine_event_start(zend_async_event_t *event);
static bool coroutine_event_stop(zend_async_event_t *event);
static bool coroutine_add_callback(zend_async_event_t *event, zend_async_event_callback_t *callback);
static bool coroutine_del_callback(zend_async_event_t *event, zend_async_event_callback_t *callback);
static bool coroutine_replay(zend_async_event_t *event,
zend_async_event_callback_t *callback,
zval *result,
zend_object **exception);
static zend_string *coroutine_info(zend_async_event_t *event);
static void coroutine_dispose(zend_async_event_t *event);
static bool coroutine_dispose(zend_async_event_t *event);

///////////////////////////////////////////////////////////
/// 2. Object Lifecycle Management
Expand Down Expand Up @@ -645,29 +645,28 @@ void async_coroutine_finalize(async_coroutine_t *coroutine)
*
* @param from_main For main coroutine
*/
void async_coroutine_suspend(const bool from_main)
bool async_coroutine_suspend(const bool from_main)
{
if (UNEXPECTED(from_main)) {
// If the Scheduler was never used, it means no coroutines were created,
// so execution can be finished without doing anything.
if (circular_buffer_is_empty(&ASYNC_G(microtasks)) && zend_hash_num_elements(&ASYNC_G(coroutines)) == 0) {
return;
return true;
}

async_scheduler_main_coroutine_suspend();
return;
return async_scheduler_main_coroutine_suspend();
}

async_scheduler_coroutine_suspend();
return async_scheduler_coroutine_suspend();
}

void async_coroutine_resume(zend_coroutine_t *coroutine, zend_object *error, const bool transfer_error)
bool async_coroutine_resume(zend_coroutine_t *coroutine, zend_object *error, const bool transfer_error)
{
zend_async_waker_t *waker = coroutine->waker;

if (UNEXPECTED(waker == NULL || waker->status == ZEND_ASYNC_WAKER_NO_STATUS)) {
async_throw_error("Cannot resume a coroutine that has not been suspended");
return;
return false;
}

if (error != NULL) {
Expand Down Expand Up @@ -695,7 +694,7 @@ void async_coroutine_resume(zend_coroutine_t *coroutine, zend_object *error, con
}

if (UNEXPECTED(waker->status == ZEND_ASYNC_WAKER_QUEUED)) {
return;
return true;
}

const bool in_scheduler_context = ZEND_ASYNC_SCHEDULER_CONTEXT;
Expand All @@ -707,12 +706,12 @@ void async_coroutine_resume(zend_coroutine_t *coroutine, zend_object *error, con
// we will execute it immediately!
if (UNEXPECTED(in_scheduler_context && coroutine == ZEND_ASYNC_CURRENT_COROUTINE)) {
waker->status = ZEND_ASYNC_WAKER_RESULT;
return;
return true;
}

if (UNEXPECTED(circular_buffer_push_ptr_with_resize(&ASYNC_G(coroutine_queue), coroutine)) == FAILURE) {
async_throw_error("Failed to enqueue coroutine");
return;
return false;
}

waker->status = ZEND_ASYNC_WAKER_QUEUED;
Expand All @@ -721,9 +720,11 @@ void async_coroutine_resume(zend_coroutine_t *coroutine, zend_object *error, con
if (in_scheduler_context) {
circular_buffer_push_ptr_with_resize(&ASYNC_G(resumed_coroutines), coroutine);
}

return true;
}

void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
bool async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
zend_object *error,
bool transfer_error,
const bool is_safely)
Expand All @@ -734,7 +735,7 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
OBJ_RELEASE(error);
}

return;
return true;
}

// An attempt to cancel a coroutine that is currently running.
Expand All @@ -757,7 +758,7 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
zend_coroutine->exception = async_new_exception(async_ce_cancellation_exception, "Coroutine cancelled");
}

return;
return true;
}

zend_async_waker_t *waker = zend_async_waker_define(zend_coroutine);
Expand All @@ -768,7 +769,7 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
error = async_new_exception(async_ce_cancellation_exception, "Coroutine cancelled");
transfer_error = true;
if (UNEXPECTED(EG(exception))) {
return;
return false;
}
}

Expand All @@ -786,7 +787,7 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
OBJ_RELEASE(error);
}

return;
return true;
}

bool was_cancelled = ZEND_COROUTINE_IS_CANCELLED(zend_coroutine);
Expand Down Expand Up @@ -815,8 +816,7 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
// In any other case, the cancellation exception overrides the existing exception.
//
ZEND_ASYNC_WAKER_APPLY_CANCELLATION(waker, error, transfer_error);
async_scheduler_coroutine_enqueue(zend_coroutine);
return;
return async_scheduler_coroutine_enqueue(zend_coroutine);
}

// In safely mode, we don't forcibly terminate the coroutine,
Expand All @@ -827,7 +827,8 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
if (transfer_error && error != NULL) {
OBJ_RELEASE(error);
}
return;

return true;
}

if (was_cancelled && waker->error != NULL &&
Expand All @@ -839,29 +840,32 @@ void async_coroutine_cancel(zend_coroutine_t *zend_coroutine,
ZEND_ASYNC_WAKER_APPLY_CANCELLATION(waker, error, transfer_error);
}

async_scheduler_coroutine_enqueue(zend_coroutine);
return async_scheduler_coroutine_enqueue(zend_coroutine);
}

///////////////////////////////////////////////////////////
/// 4. Event System Interface
///////////////////////////////////////////////////////////

static void coroutine_event_start(zend_async_event_t *event)
static bool coroutine_event_start(zend_async_event_t *event)
{
return true;
}

static void coroutine_event_stop(zend_async_event_t *event)
static bool coroutine_event_stop(zend_async_event_t *event)
{
// Empty implementation - coroutines don't need explicit stop
return true;
}

static void coroutine_add_callback(zend_async_event_t *event, zend_async_event_callback_t *callback)
static bool coroutine_add_callback(zend_async_event_t *event, zend_async_event_callback_t *callback)
{
zend_async_callbacks_push(event, callback);
return zend_async_callbacks_push(event, callback);
}

static void coroutine_del_callback(zend_async_event_t *event, zend_async_event_callback_t *callback)
static bool coroutine_del_callback(zend_async_event_t *event, zend_async_event_callback_t *callback)
{
zend_async_callbacks_remove(event, callback);
return zend_async_callbacks_remove(event, callback);
}

/**
Expand Down Expand Up @@ -930,10 +934,11 @@ static zend_string *coroutine_info(zend_async_event_t *event)
}
}

static void coroutine_dispose(zend_async_event_t *event)
static bool coroutine_dispose(zend_async_event_t *event)
{
async_coroutine_t *coroutine = (async_coroutine_t *) event;
OBJ_RELEASE(&coroutine->std);
return true;
}

///////////////////////////////////////////////////////////
Expand Down
Loading
Loading