Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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