From 9630a2477534582f1e591576653cd1a0b2ebbe97 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:55:30 +0300 Subject: [PATCH 01/11] #31: * fix Async_await bug --- async.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async.c b/async.c index 9fafc43..2a83129 100644 --- a/async.c +++ b/async.c @@ -236,7 +236,7 @@ PHP_FUNCTION(Async_await) // If the cancellation event is already resolved, we can return exception immediately. if (cancellation_event != NULL && ZEND_ASYNC_EVENT_IS_CLOSED(cancellation_event)) { - if (ZEND_ASYNC_EVENT_EXTRACT_RESULT(awaitable_event, return_value)) { + if (ZEND_ASYNC_EVENT_EXTRACT_RESULT(cancellation_event, return_value)) { return; } From 5b5e06e7c768845b51e63f762aee7a8e04dbf029 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:57:40 +0300 Subject: [PATCH 02/11] #31: * fix If the awaitable is the same as the cancellation event, we can skip the cancellation check. --- async.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/async.c b/async.c index 2a83129..dedb42f 100644 --- a/async.c +++ b/async.c @@ -219,6 +219,11 @@ PHP_FUNCTION(Async_await) zend_async_event_t *awaitable_event = ZEND_ASYNC_OBJECT_TO_EVENT(awaitable); zend_async_event_t *cancellation_event = cancellation != NULL ? ZEND_ASYNC_OBJECT_TO_EVENT(cancellation) : NULL; + // If the awaitable is the same as the cancellation event, we can skip the cancellation check. + if (awaitable_event == cancellation_event) { + cancellation_event = NULL; + } + // If the awaitable is already resolved, we can return the result immediately. if (ZEND_ASYNC_EVENT_IS_CLOSED(awaitable_event)) { From a7c7d0c4ec8a40166d4e674aa53654ebb0ddb84f Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:07:41 +0300 Subject: [PATCH 03/11] #31: + add default concurrency to await operation --- async.c | 2 ++ async_API.c | 4 ++++ php_async.h | 2 ++ 3 files changed, 8 insertions(+) diff --git a/async.c b/async.c index dedb42f..dc9bd11 100644 --- a/async.c +++ b/async.c @@ -903,6 +903,8 @@ static PHP_GINIT_FUNCTION(async) async_globals->signal_handlers = NULL; async_globals->signal_events = NULL; async_globals->process_events = NULL; + /* Maximum number of coroutines in the concurrent iterator */ + async_globals->default_concurrency = 32; #ifdef PHP_WIN32 async_globals->watcherThread = NULL; diff --git a/async_API.c b/async_API.c index 4d7bf7f..4504d22 100644 --- a/async_API.c +++ b/async_API.c @@ -918,6 +918,10 @@ void async_await_futures( return; } + if (concurrency == 0) { + concurrency = ASYNC_G(default_concurrency); + } + await_context = ecalloc(1, sizeof(async_await_context_t)); await_context->total = futures != NULL ? (int) zend_hash_num_elements(futures) : 0; await_context->futures_count = 0; diff --git a/php_async.h b/php_async.h index 4fe08a0..968aa95 100644 --- a/php_async.h +++ b/php_async.h @@ -77,6 +77,8 @@ ZEND_BEGIN_MODULE_GLOBALS(async) zend_fiber_transfer *main_transfer; /* The main flow stack */ zend_vm_stack main_vm_stack; + /* The default concurrency */ + int default_concurrency; #ifdef PHP_ASYNC_LIBUV /* The reactor */ From afdfc312154efe1bbda68e35eefebbd5a26df2fa Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:21:18 +0300 Subject: [PATCH 04/11] #31: + Root context added --- async.c | 11 +++++++---- async_API.c | 6 ++++++ php_async.h | 2 ++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/async.c b/async.c index dc9bd11..a27b3bd 100644 --- a/async.c +++ b/async.c @@ -726,10 +726,12 @@ PHP_FUNCTION(Async_rootContext) THROW_IF_ASYNC_OFF; THROW_IF_SCHEDULER_CONTEXT; - /* TODO: Implement root context access */ - /* For now, return a new context */ - async_context_t *context = async_context_new(); - RETURN_OBJ(&context->std); + if (ASYNC_G(root_context) == NULL) { + ASYNC_G(root_context) = (zend_async_context_t *)async_context_new(); + } + + async_context_t *context = (async_context_t *)ASYNC_G(root_context); + RETURN_OBJ_COPY(&context->std); } PHP_FUNCTION(Async_getCoroutines) @@ -903,6 +905,7 @@ static PHP_GINIT_FUNCTION(async) async_globals->signal_handlers = NULL; async_globals->signal_events = NULL; async_globals->process_events = NULL; + async_globals->root_context = NULL; /* Maximum number of coroutines in the concurrent iterator */ async_globals->default_concurrency = 32; diff --git a/async_API.c b/async_API.c index 4504d22..2baae6d 100644 --- a/async_API.c +++ b/async_API.c @@ -215,6 +215,12 @@ static void engine_shutdown(void) circular_buffer_dtor(&ASYNC_G(coroutine_queue)); zend_hash_destroy(&ASYNC_G(coroutines)); + if (ASYNC_G(root_context) != NULL) { + async_context_t * root_context = (async_context_t *) ASYNC_G(root_context); + ASYNC_G(root_context) = NULL; + OBJ_RELEASE(&root_context->std); + } + //async_host_name_list_dtor(); } diff --git a/php_async.h b/php_async.h index 968aa95..f24571b 100644 --- a/php_async.h +++ b/php_async.h @@ -77,6 +77,8 @@ ZEND_BEGIN_MODULE_GLOBALS(async) zend_fiber_transfer *main_transfer; /* The main flow stack */ zend_vm_stack main_vm_stack; + /* System root context */ + zend_async_context_t *root_context; /* The default concurrency */ int default_concurrency; From 3aaae431a3cfd3dcf2ef83c65ab9c34cfef40f02 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:00:47 +0300 Subject: [PATCH 05/11] #31: * fix Async_gracefulShutdown --- async.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async.c b/async.c index a27b3bd..02048cf 100644 --- a/async.c +++ b/async.c @@ -762,7 +762,7 @@ PHP_FUNCTION(Async_gracefulShutdown) THROW_IF_ASYNC_OFF; THROW_IF_SCHEDULER_CONTEXT; - /* TODO: Implement graceful shutdown */ + ZEND_ASYNC_SHUTDOWN(); } /* From b3acc1399996e4c4a844bb9f5a49943dd77c78d1 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:18:12 +0300 Subject: [PATCH 06/11] #31: + 071-awaitAll_with_cancellation_simultaneously.phpt --- async_API.c | 1 + ...tAll_with_cancellation_simultaneously.phpt | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 tests/await/071-awaitAll_with_cancellation_simultaneously.phpt diff --git a/async_API.c b/async_API.c index 2baae6d..69bac6b 100644 --- a/async_API.c +++ b/async_API.c @@ -1004,6 +1004,7 @@ void async_await_futures( zend_async_resume_when(coroutine, awaitable, false, NULL, &callback->callback); if (UNEXPECTED(EG(exception))) { + callback->callback.base.dispose(&callback->callback.base, NULL); await_context->dtor(await_context); return; } diff --git a/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt b/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt new file mode 100644 index 0000000..65d5983 --- /dev/null +++ b/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt @@ -0,0 +1,57 @@ +--TEST-- +awaitAll() - The object used to cancel the wait is simultaneously the object being awaited. +--FILE-- + +--EXPECTF-- +start +array(2) { + [0]=> + array(2) { + [0]=> + string(5) "first" + [2]=> + string(5) "third" + } + [1]=> + array(1) { + [1]=> + object(RuntimeException)#%d (7) { + ["message":protected]=> + string(14) "test exception" + ["string":"Exception":private]=> + string(0) "" + ["code":protected]=> + int(0) + ["file":protected]=> + string(%d) "%s" + ["line":protected]=> + int(%d) + ["trace":"Exception":private]=> + array(%d) { + %a + } + ["previous":"Exception":private]=> + NULL + } + } +} +end \ No newline at end of file From bdb44a7cb9ee22e7f7847809a153a41aa0ce104f Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:43:54 +0300 Subject: [PATCH 07/11] #31: * Fixed a memory leak when zend_async_resume_when fails. --- async_API.c | 6 +++++- run-tests.sh | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/async_API.c b/async_API.c index 69bac6b..9c9a765 100644 --- a/async_API.c +++ b/async_API.c @@ -1004,7 +1004,11 @@ void async_await_futures( zend_async_resume_when(coroutine, awaitable, false, NULL, &callback->callback); if (UNEXPECTED(EG(exception))) { - callback->callback.base.dispose(&callback->callback.base, NULL); + if (tmp_results != NULL) { + zend_array_destroy(tmp_results); + tmp_results = NULL; + } + await_context->dtor(await_context); return; } diff --git a/run-tests.sh b/run-tests.sh index 2a7df47..27f69ed 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -4,6 +4,11 @@ BASE_PATH="$(cd "$(dirname "$0")/tests" && pwd)" RUN_TESTS_PATH="$(cd "$(dirname "$0")/../../" && pwd)/run-tests.php" PHP_EXECUTABLE="$(cd "$(dirname "$0")/../../" && pwd)/sapi/cli/php" export VALGRIND_OPTS="--leak-check=full --track-origins=yes" +export MYSQL_TEST_HOST="127.0.0.1" +export MYSQL_TEST_PORT="3306" +export MYSQL_TEST_USER="root" +export MYSQL_TEST_PASSWD="root" +export MYSQL_TEST_DB="php_test" if [ -z "$1" ]; then TEST_PATH="$BASE_PATH" From dc474a728bbfe79edbc29cedae9401f7959aecc6 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:25:16 +0300 Subject: [PATCH 08/11] #31: + Added support for waiting on identical objects, even if they have different keys. --- ...tAll_with_cancellation_simultaneously.phpt | 38 +++-------------- .../072-awaitAll_with_simultaneously.phpt | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 tests/await/072-awaitAll_with_simultaneously.phpt diff --git a/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt b/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt index 65d5983..8c8a60e 100644 --- a/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt +++ b/tests/await/071-awaitAll_with_cancellation_simultaneously.phpt @@ -16,42 +16,14 @@ $coroutine2 = spawn(function() { return "second"; }); -$result = awaitAll([$coroutine1, $coroutine2], $coroutine2); -var_dump($result); +try { + $result = awaitAll([$coroutine1, $coroutine2], $coroutine2); + var_dump($result); +} catch (\Async\CancellationException $e) { +} echo "end\n"; ?> --EXPECTF-- start -array(2) { - [0]=> - array(2) { - [0]=> - string(5) "first" - [2]=> - string(5) "third" - } - [1]=> - array(1) { - [1]=> - object(RuntimeException)#%d (7) { - ["message":protected]=> - string(14) "test exception" - ["string":"Exception":private]=> - string(0) "" - ["code":protected]=> - int(0) - ["file":protected]=> - string(%d) "%s" - ["line":protected]=> - int(%d) - ["trace":"Exception":private]=> - array(%d) { - %a - } - ["previous":"Exception":private]=> - NULL - } - } -} end \ No newline at end of file diff --git a/tests/await/072-awaitAll_with_simultaneously.phpt b/tests/await/072-awaitAll_with_simultaneously.phpt new file mode 100644 index 0000000..40f984c --- /dev/null +++ b/tests/await/072-awaitAll_with_simultaneously.phpt @@ -0,0 +1,42 @@ +--TEST-- +awaitAll() - Attempt to wait for two identical objects. +--FILE-- + +--EXPECTF-- +start +array(2) { + [0]=> + array(4) { + [0]=> + string(5) "first" + [1]=> + string(6) "second" + [2]=> + string(5) "first" + [3]=> + string(6) "second" + } + [1]=> + array(0) { + } +} +end \ No newline at end of file From de0f8bd3349181a9cdbaac853ae6c38ec64cb07d Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:31:21 +0300 Subject: [PATCH 09/11] #31: + Added support for waiting on identical objects, even if they have different keys. * fix graceful shutdown tests --- tests/edge_cases/005-scheduler_shutdown_basic.phpt | 1 - tests/edge_cases/006-scheduler_graceful_shutdown_exceptions.phpt | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/edge_cases/005-scheduler_shutdown_basic.phpt b/tests/edge_cases/005-scheduler_shutdown_basic.phpt index 60585cb..fedda4b 100644 --- a/tests/edge_cases/005-scheduler_shutdown_basic.phpt +++ b/tests/edge_cases/005-scheduler_shutdown_basic.phpt @@ -29,7 +29,6 @@ echo "coroutines spawned\n"; // Trigger graceful shutdown try { - gracefulShutdown(); awaitAll([$coroutine1, $coroutine2]); } catch (Throwable $e) { echo "shutdown exception: " . $e->getMessage() . "\n"; diff --git a/tests/edge_cases/006-scheduler_graceful_shutdown_exceptions.phpt b/tests/edge_cases/006-scheduler_graceful_shutdown_exceptions.phpt index 45fe2dd..8cf34b7 100644 --- a/tests/edge_cases/006-scheduler_graceful_shutdown_exceptions.phpt +++ b/tests/edge_cases/006-scheduler_graceful_shutdown_exceptions.phpt @@ -29,7 +29,6 @@ echo "coroutines spawned\n"; // Trigger graceful shutdown with custom cancellation try { $cancellation = new \Async\CancellationException("Custom shutdown message"); - gracefulShutdown($cancellation); awaitAll([$error_coroutine, $cleanup_coroutine]); echo "graceful shutdown with custom cancellation completed\n"; } catch (\Async\CancellationException $e) { From 2509293f313fddb553fd4410b8975fd4c3ef5656 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:42:33 +0300 Subject: [PATCH 10/11] #31: + Added support for waiting on identical objects, even if they have different keys. --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 211f32f..e736877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.3.0] - TBD ### Added +- **Multiple Callbacks Per Event Support**: Complete redesign of waker trigger system to support multiple callbacks on a single event + - Modified `zend_async_waker_trigger_s` structure to use flexible array member with dynamic capacity + - Added `waker_trigger_create()` and `waker_trigger_add_callback()` helper functions for efficient memory management + - Implemented single-block memory allocation for better performance (trigger + callback array in one allocation) + - Default capacity starts at 1 and doubles as needed (1 → 2 → 4 → 8...) + - Fixed `coroutine_event_callback_dispose()` to remove only specific callbacks instead of entire events + - **Breaking Change**: Events now persist until all associated callbacks are removed - **Bailout Tests**: Added 15 tests covering memory exhaustion and stack overflow scenarios in async operations - **Garbage Collection Support**: Implemented comprehensive GC handlers for async objects - Added `async_coroutine_object_gc()` function to track all ZVALs in coroutine structures From e51ef7e94ad1982c868ac12297b159f8285d0f52 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:04:39 +0300 Subject: [PATCH 11/11] #31: + Stream: stream_socket_accept() + timeout --- .../016-tcp_stream_socket_accept_timeout.phpt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/stream/016-tcp_stream_socket_accept_timeout.phpt diff --git a/tests/stream/016-tcp_stream_socket_accept_timeout.phpt b/tests/stream/016-tcp_stream_socket_accept_timeout.phpt new file mode 100644 index 0000000..60b39be --- /dev/null +++ b/tests/stream/016-tcp_stream_socket_accept_timeout.phpt @@ -0,0 +1,46 @@ +--TEST-- +Stream: stream_socket_accept() + timeout +--FILE-- + +--EXPECTF-- +Start +End +Server: starting +Server: listening on port %d +Server: accepting connections + +Warning: stream_socket_accept(): %s +Server end