From b44b96a1d254c880e0f67aaa681c3ba09606afc5 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:30:01 +0200 Subject: [PATCH 01/25] Refactor coroutine header includes: update fiber and async API dependencies --- coroutine.h | 1 + 1 file changed, 1 insertion(+) diff --git a/coroutine.h b/coroutine.h index 8eacb62..fdc933a 100644 --- a/coroutine.h +++ b/coroutine.h @@ -17,6 +17,7 @@ #define COROUTINE_H #include "php_async_api.h" +#include #include /* Fiber context structure for pooling */ From f26fbe244e79377675d2bc8c78f5ff4356e374b8 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:45:36 +0200 Subject: [PATCH 02/25] Add basic fiber creation and execution test with active async scheduler --- scheduler.c | 12 +++++- .../fiber/001-fiber_with_coroutine_basic.phpt | 39 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/fiber/001-fiber_with_coroutine_basic.phpt diff --git a/scheduler.c b/scheduler.c index c661734..9712c86 100644 --- a/scheduler.c +++ b/scheduler.c @@ -1025,6 +1025,16 @@ bool async_scheduler_coroutine_enqueue(zend_coroutine_t *coroutine) // save the filename and line number where the coroutine was created zend_apply_current_filename_and_line(&coroutine->filename, &coroutine->lineno); + // Notify scope that a new coroutine has been enqueued + zend_async_scope_t *scope = coroutine->scope; + + if (UNEXPECTED(scope == NULL)) { + // throw error if the coroutine has no scope + coroutine->waker->status = ZEND_ASYNC_WAKER_NO_STATUS; + async_throw_error("The coroutine has no scope assigned"); + return false; + } + if (UNEXPECTED(zend_hash_index_add_ptr(&ASYNC_G(coroutines), ((async_coroutine_t *)coroutine)->std.handle, coroutine) == NULL)) { coroutine->waker->status = ZEND_ASYNC_WAKER_IGNORED; async_throw_error("Failed to add coroutine to the list"); @@ -1033,8 +1043,6 @@ bool async_scheduler_coroutine_enqueue(zend_coroutine_t *coroutine) ZEND_ASYNC_INCREASE_COROUTINE_COUNT; - // Notify scope that a new coroutine has been enqueued - zend_async_scope_t *scope = coroutine->scope; scope->after_coroutine_enqueue(coroutine, scope); if (UNEXPECTED(EG(exception))) { coroutine->waker->status = ZEND_ASYNC_WAKER_IGNORED; diff --git a/tests/fiber/001-fiber_with_coroutine_basic.phpt b/tests/fiber/001-fiber_with_coroutine_basic.phpt new file mode 100644 index 0000000..e5bbe48 --- /dev/null +++ b/tests/fiber/001-fiber_with_coroutine_basic.phpt @@ -0,0 +1,39 @@ +--TEST-- +Fiber with coroutine: Basic fiber creation and execution when async is active +--FILE-- +start(); + echo "Fiber returned: " . $result . "\n"; + + return "coroutine result"; +}); + +$result = await($coroutine); +echo "Coroutine completed with: " . $result . "\n"; + +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber creation with active async scheduler +Coroutine started +Starting fiber +Fiber executing +Fiber returned: fiber result +Coroutine completed with: coroutine result +Test completed \ No newline at end of file From b2e55d5e3793d2dcf93329cdaec0b930fdeb881d Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:01:30 +0200 Subject: [PATCH 03/25] Add tests for fiber execution: simple return and suspend/resume cycles --- tests/fiber/002-fiber_simple_return.phpt | 30 +++++++++++++++ tests/fiber/003-fiber_one_suspend_resume.phpt | 37 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/fiber/002-fiber_simple_return.phpt create mode 100644 tests/fiber/003-fiber_one_suspend_resume.phpt diff --git a/tests/fiber/002-fiber_simple_return.phpt b/tests/fiber/002-fiber_simple_return.phpt new file mode 100644 index 0000000..c98aad3 --- /dev/null +++ b/tests/fiber/002-fiber_simple_return.phpt @@ -0,0 +1,30 @@ +--TEST-- +Fiber with simple return without suspend +--FILE-- +start(); + echo "Got: " . $result . "\n"; + + return "coroutine done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber simple return without suspend +Fiber executing +Got: result from fiber +Test completed diff --git a/tests/fiber/003-fiber_one_suspend_resume.phpt b/tests/fiber/003-fiber_one_suspend_resume.phpt new file mode 100644 index 0000000..fc10069 --- /dev/null +++ b/tests/fiber/003-fiber_one_suspend_resume.phpt @@ -0,0 +1,37 @@ +--TEST-- +UC-003: One suspend/resume cycle +--FILE-- +start(); + echo "Fiber suspended with: " . $suspended . "\n"; + + $result = $fiber->resume("resume value"); + echo "Fiber returned: " . $result . "\n"; + + return "complete"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: One suspend/resume cycle +Before suspend +Fiber suspended with: suspended +After resume, got: resume value +Fiber returned: done +Test completed From 9438aea4d009ec34955cd388db2125471b5a6521 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:44:31 +0200 Subject: [PATCH 04/25] Add tests for multiple suspend/resume cycles and nested fibers --- tests/fiber/004-fiber_multiple_suspends.phpt | 45 +++++++++++++++ tests/fiber/005-fiber_nested_spawn.phpt | 45 +++++++++++++++ tests/fiber/006-fiber_nested_fibers.phpt | 48 ++++++++++++++++ tests/fiber/007-fiber_concurrent_fibers.phpt | 60 ++++++++++++++++++++ tests/fiber/008-fiber_exception_natural.phpt | 34 +++++++++++ 5 files changed, 232 insertions(+) create mode 100644 tests/fiber/004-fiber_multiple_suspends.phpt create mode 100644 tests/fiber/005-fiber_nested_spawn.phpt create mode 100644 tests/fiber/006-fiber_nested_fibers.phpt create mode 100644 tests/fiber/007-fiber_concurrent_fibers.phpt create mode 100644 tests/fiber/008-fiber_exception_natural.phpt diff --git a/tests/fiber/004-fiber_multiple_suspends.phpt b/tests/fiber/004-fiber_multiple_suspends.phpt new file mode 100644 index 0000000..6bfb8a8 --- /dev/null +++ b/tests/fiber/004-fiber_multiple_suspends.phpt @@ -0,0 +1,45 @@ +--TEST-- +Multiple suspend/resume cycles +--FILE-- +start() . "\n"; + echo "R2: " . $fiber->resume("resume-1") . "\n"; + echo "R3: " . $fiber->resume("resume-2") . "\n"; + echo "R4: " . $fiber->resume("resume-3") . "\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Multiple suspend/resume cycles +R1: suspend-1 +Got: resume-1 +R2: suspend-2 +Got: resume-2 +R3: suspend-3 +Got: resume-3 +R4: final +Test completed diff --git a/tests/fiber/005-fiber_nested_spawn.phpt b/tests/fiber/005-fiber_nested_spawn.phpt new file mode 100644 index 0000000..d282742 --- /dev/null +++ b/tests/fiber/005-fiber_nested_spawn.phpt @@ -0,0 +1,45 @@ +--TEST-- +Nested spawn inside Fiber +--FILE-- +start(); + echo "Fiber completed: " . $result . "\n"; + + return "outer done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Nested spawn inside Fiber +Fiber: spawning inner coroutine +Inner: started +Inner: resumed +Fiber: inner returned: inner result +Fiber completed: fiber done +Test completed diff --git a/tests/fiber/006-fiber_nested_fibers.phpt b/tests/fiber/006-fiber_nested_fibers.phpt new file mode 100644 index 0000000..155b7e7 --- /dev/null +++ b/tests/fiber/006-fiber_nested_fibers.phpt @@ -0,0 +1,48 @@ +--TEST-- +Nested Fibers (Fiber inside Fiber) +--FILE-- +start(); + echo "Inner suspended: " . $val . "\n"; + + $result = $inner->resume(); + echo "Inner result: " . $result . "\n"; + + return "outer done"; + }); + + $result = $outer->start(); + echo "Outer result: " . $result . "\n"; + + return "complete"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Nested Fibers +Outer fiber started +Inner fiber started +Inner suspended: inner suspend +Inner fiber resumed +Inner result: inner done +Outer result: outer done +Test completed diff --git a/tests/fiber/007-fiber_concurrent_fibers.phpt b/tests/fiber/007-fiber_concurrent_fibers.phpt new file mode 100644 index 0000000..8f2f1ef --- /dev/null +++ b/tests/fiber/007-fiber_concurrent_fibers.phpt @@ -0,0 +1,60 @@ +--TEST-- +UC-007: Multiple concurrent Fibers +--FILE-- +start(); + $fiber2->start(); + + echo "Resume F2\n"; + $r2 = $fiber2->resume(); + echo "F2 result: " . $r2 . "\n"; + + echo "Resume F1 (1)\n"; + $fiber1->resume(); + + echo "Resume F1 (2)\n"; + $r1 = $fiber1->resume(); + echo "F1 result: " . $r1 . "\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Concurrent Fibers +F1: step 1 +F2: step 1 +Resume F2 +F2: step 2 +F2 result: fiber2 done +Resume F1 (1) +F1: step 2 +Resume F1 (2) +F1: step 3 +F1 result: fiber1 done +Test completed diff --git a/tests/fiber/008-fiber_exception_natural.phpt b/tests/fiber/008-fiber_exception_natural.phpt new file mode 100644 index 0000000..7693d4c --- /dev/null +++ b/tests/fiber/008-fiber_exception_natural.phpt @@ -0,0 +1,34 @@ +--TEST-- +UC-008: Natural exception inside Fiber +--FILE-- +start(); + echo "Should not reach here\n"; + } catch (Exception $e) { + echo "Caught: " . $e->getMessage() . "\n"; + } + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Natural exception in Fiber +Before exception +Caught: Fiber error +Test completed From d99e1fb08a35e08395cf10c21b3929ec9513652a Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:49:51 +0200 Subject: [PATCH 05/25] Add tests for Fiber exception handling and propagation --- tests/fiber/007-fiber_concurrent_fibers.phpt | 2 +- tests/fiber/008-fiber_exception_natural.phpt | 2 +- tests/fiber/009-fiber_throw_method.phpt | 44 ++++++++++++++++++ .../010-fiber_exception_propagation.phpt | 41 +++++++++++++++++ .../011-fiber_exception_nested_spawn.phpt | 45 +++++++++++++++++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 tests/fiber/009-fiber_throw_method.phpt create mode 100644 tests/fiber/010-fiber_exception_propagation.phpt create mode 100644 tests/fiber/011-fiber_exception_nested_spawn.phpt diff --git a/tests/fiber/007-fiber_concurrent_fibers.phpt b/tests/fiber/007-fiber_concurrent_fibers.phpt index 8f2f1ef..969c110 100644 --- a/tests/fiber/007-fiber_concurrent_fibers.phpt +++ b/tests/fiber/007-fiber_concurrent_fibers.phpt @@ -1,5 +1,5 @@ --TEST-- -UC-007: Multiple concurrent Fibers +Multiple concurrent Fibers --FILE-- getMessage() . "\n"; + return "recovered"; + } + + return "normal"; + }); + + $val = $fiber->start(); + echo "Suspended: " . $val . "\n"; + + $result = $fiber->throw(new Exception("thrown error")); + echo "Result: " . $result . "\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber throw method +Before suspend +Suspended: waiting +Caught in fiber: thrown error +Result: recovered +Test completed diff --git a/tests/fiber/010-fiber_exception_propagation.phpt b/tests/fiber/010-fiber_exception_propagation.phpt new file mode 100644 index 0000000..b8bd5e5 --- /dev/null +++ b/tests/fiber/010-fiber_exception_propagation.phpt @@ -0,0 +1,41 @@ +--TEST-- +Exception propagation from Fiber to coroutine +--FILE-- +start(); + echo "Got: " . $val . "\n"; + + $fiber->resume("resume"); + echo "Should not print\n"; + } catch (RuntimeException $e) { + echo "Caught in coroutine: " . $e->getMessage() . "\n"; + } + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Exception propagation +Fiber: suspending +Got: suspend +Fiber: throwing +Caught in coroutine: fiber exception +Test completed diff --git a/tests/fiber/011-fiber_exception_nested_spawn.phpt b/tests/fiber/011-fiber_exception_nested_spawn.phpt new file mode 100644 index 0000000..d8ce4d0 --- /dev/null +++ b/tests/fiber/011-fiber_exception_nested_spawn.phpt @@ -0,0 +1,45 @@ +--TEST-- +Exception in nested spawn inside Fiber +--FILE-- +getMessage() . "\n"; + } + + return "fiber recovered"; + }); + + $result = $fiber->start(); + echo "Result: " . $result . "\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Exception in nested spawn +Fiber: spawning +Inner: throwing +Caught in fiber: inner exception +Result: fiber recovered +Test completed From d5e78126e153fd67df2239a87858603e704c19a4 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:03:11 +0200 Subject: [PATCH 06/25] + Fiber status methods --- tests/fiber/012-fiber_status_methods.phpt | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/fiber/012-fiber_status_methods.phpt diff --git a/tests/fiber/012-fiber_status_methods.phpt b/tests/fiber/012-fiber_status_methods.phpt new file mode 100644 index 0000000..6084d86 --- /dev/null +++ b/tests/fiber/012-fiber_status_methods.phpt @@ -0,0 +1,64 @@ +--TEST-- +Fiber status methods +--FILE-- +isStarted() ? "Y" : "N") . "\n"; + echo "isSuspended: " . ($fiber->isSuspended() ? "Y" : "N") . "\n"; + echo "isRunning: " . ($fiber->isRunning() ? "Y" : "N") . "\n"; + echo "isTerminated: " . ($fiber->isTerminated() ? "Y" : "N") . "\n"; + + $fiber->start(); + + echo "=== After suspend ===\n"; + echo "isStarted: " . ($fiber->isStarted() ? "Y" : "N") . "\n"; + echo "isSuspended: " . ($fiber->isSuspended() ? "Y" : "N") . "\n"; + echo "isRunning: " . ($fiber->isRunning() ? "Y" : "N") . "\n"; + echo "isTerminated: " . ($fiber->isTerminated() ? "Y" : "N") . "\n"; + + $fiber->resume(); + $fiber->resume(); + + echo "=== After terminated ===\n"; + echo "isStarted: " . ($fiber->isStarted() ? "Y" : "N") . "\n"; + echo "isSuspended: " . ($fiber->isSuspended() ? "Y" : "N") . "\n"; + echo "isRunning: " . ($fiber->isRunning() ? "Y" : "N") . "\n"; + echo "isTerminated: " . ($fiber->isTerminated() ? "Y" : "N") . "\n"; + + return "complete"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber status methods +=== Before start === +isStarted: N +isSuspended: N +isRunning: N +isTerminated: N +=== After suspend === +isStarted: Y +isSuspended: Y +isRunning: N +isTerminated: N +=== After terminated === +isStarted: Y +isSuspended: N +isRunning: N +isTerminated: Y +Test completed From b7d8bff255960cc6d6e584f01ba49da77f4609e1 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:08:03 +0200 Subject: [PATCH 07/25] Add tests for Fiber methods: getReturn, garbage collection, and handling NULL values --- tests/fiber/003-fiber_one_suspend_resume.phpt | 2 +- tests/fiber/013-fiber_getReturn.phpt | 32 ++++++++++++++ tests/fiber/014-fiber_gc_suspended.phpt | 38 +++++++++++++++++ tests/fiber/015-fiber_null_values.phpt | 42 +++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/fiber/013-fiber_getReturn.phpt create mode 100644 tests/fiber/014-fiber_gc_suspended.phpt create mode 100644 tests/fiber/015-fiber_null_values.phpt diff --git a/tests/fiber/003-fiber_one_suspend_resume.phpt b/tests/fiber/003-fiber_one_suspend_resume.phpt index fc10069..57f5dab 100644 --- a/tests/fiber/003-fiber_one_suspend_resume.phpt +++ b/tests/fiber/003-fiber_one_suspend_resume.phpt @@ -1,5 +1,5 @@ --TEST-- -UC-003: One suspend/resume cycle +One suspend/resume cycle --FILE-- start(); + $fiber->resume(); + + $result = $fiber->getReturn(); + echo "getReturn: " . $result . "\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber getReturn +getReturn: final result +Test completed diff --git a/tests/fiber/014-fiber_gc_suspended.phpt b/tests/fiber/014-fiber_gc_suspended.phpt new file mode 100644 index 0000000..3e09866 --- /dev/null +++ b/tests/fiber/014-fiber_gc_suspended.phpt @@ -0,0 +1,38 @@ +--TEST-- +UC-014: Fiber garbage collection when suspended +--FILE-- +start(); + echo "Fiber suspended\n"; + + // Release reference + unset($fiber); + gc_collect_cycles(); + + echo "Fiber GC'd\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber GC when suspended +Before suspend +Fiber suspended +Fiber GC'd +Test completed diff --git a/tests/fiber/015-fiber_null_values.phpt b/tests/fiber/015-fiber_null_values.phpt new file mode 100644 index 0000000..f4f8d5e --- /dev/null +++ b/tests/fiber/015-fiber_null_values.phpt @@ -0,0 +1,42 @@ +--TEST-- +UC-015: Fiber with NULL values in suspend/resume +--FILE-- +start(); + echo "Suspend value is null: " . ($s1 === null ? "Y" : "N") . "\n"; + + $s2 = $fiber->resume(); + echo "Suspend value is null: " . ($s2 === null ? "Y" : "N") . "\n"; + + $r = $fiber->resume(null); + echo "Return value is null: " . ($r === null ? "Y" : "N") . "\n"; + + return "done"; +}); + +await($coroutine); +echo "Test completed\n"; +?> +--EXPECT-- +Test: NULL values in suspend/resume +Suspend value is null: Y +Resume value is null: Y +Suspend value is null: Y +Return value is null: Y +Test completed From e73a1a6fe90e6b7d845d0158955aa9f49a24a9f6 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:09:30 +0200 Subject: [PATCH 08/25] Refactor Fiber garbage collection and NULL value handling --- tests/fiber/014-fiber_gc_suspended.phpt | 2 +- tests/fiber/015-fiber_null_values.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fiber/014-fiber_gc_suspended.phpt b/tests/fiber/014-fiber_gc_suspended.phpt index 3e09866..4dccb4a 100644 --- a/tests/fiber/014-fiber_gc_suspended.phpt +++ b/tests/fiber/014-fiber_gc_suspended.phpt @@ -1,5 +1,5 @@ --TEST-- -UC-014: Fiber garbage collection when suspended +Fiber garbage collection when suspended --FILE-- Date: Sat, 13 Dec 2025 19:29:17 +0200 Subject: [PATCH 09/25] Add fiber tests for independent coroutines - 016: Nested fibers in separate coroutines with symmetric switching - 017: Multiple suspend/resume in different coroutines - 018: GC with circular references between fibers and coroutines Tests verify that fibers in different coroutines are independent and properly delegate GC to coroutine handlers. --- ...6-fiber_nested_in_separate_coroutines.phpt | 63 +++++++++++++++++++ .../fiber/017-fiber_coroutine_gc_cycles.phpt | 56 +++++++++++++++++ .../017-fiber_suspend_resume_multiple.phpt | 49 +++++++++++++++ .../fiber/018-fiber_coroutine_gc_cycles.phpt | 56 +++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 tests/fiber/016-fiber_nested_in_separate_coroutines.phpt create mode 100644 tests/fiber/017-fiber_coroutine_gc_cycles.phpt create mode 100644 tests/fiber/017-fiber_suspend_resume_multiple.phpt create mode 100644 tests/fiber/018-fiber_coroutine_gc_cycles.phpt diff --git a/tests/fiber/016-fiber_nested_in_separate_coroutines.phpt b/tests/fiber/016-fiber_nested_in_separate_coroutines.phpt new file mode 100644 index 0000000..a1e4c40 --- /dev/null +++ b/tests/fiber/016-fiber_nested_in_separate_coroutines.phpt @@ -0,0 +1,63 @@ +--TEST-- +Nested fibers in separate coroutines (symmetric switching) +--FILE-- +start(); + echo "C1-O-middle\n"; + $inner->resume(); + echo "C1-O-end\n"; + }); + + $outer->start(); +}); + +$c2 = spawn(function() { + $outer = new Fiber(function() { + echo "C2-O-start\n"; + + $inner = new Fiber(function() { + echo "C2-I-start\n"; + Fiber::suspend(); + echo "C2-I-resume\n"; + }); + + $inner->start(); + echo "C2-O-middle\n"; + $inner->resume(); + echo "C2-O-end\n"; + }); + + $outer->start(); +}); + +await($c1); +await($c2); + +echo "OK\n"; +?> +--EXPECT-- +C1-O-start +C2-O-start +C1-I-start +C2-I-start +C1-O-middle +C2-O-middle +C1-I-resume +C2-I-resume +C1-O-end +C2-O-end +OK diff --git a/tests/fiber/017-fiber_coroutine_gc_cycles.phpt b/tests/fiber/017-fiber_coroutine_gc_cycles.phpt new file mode 100644 index 0000000..2ad33af --- /dev/null +++ b/tests/fiber/017-fiber_coroutine_gc_cycles.phpt @@ -0,0 +1,56 @@ +--TEST-- +Fiber and Coroutine GC with circular references +--FILE-- +data = str_repeat("x", 1000); + } +} + +$c = spawn(function() { + $node1 = new Node(); + $node2 = new Node(); + + // Create circular reference + $node1->ref = $node2; + $node2->ref = $node1; + + // Store in fiber + $node1->fiber = new Fiber(function() use ($node1, $node2) { + echo "F-start\n"; + Fiber::suspend($node2); + echo "F-resume\n"; + return $node1; + }); + + $result = $node1->fiber->start(); + echo "Got: " . ($result === $node2 ? "node2" : "other") . "\n"; + + $result = $node1->fiber->resume(); + echo "Got: " . ($result === $node1 ? "node1" : "other") . "\n"; + + // Break references and trigger GC + $node1 = null; + $node2 = null; +}); + +await($c); +gc_collect_cycles(); + +echo "OK\n"; +?> +--EXPECT-- +F-start +Got: node2 +F-resume +Got: node1 +OK diff --git a/tests/fiber/017-fiber_suspend_resume_multiple.phpt b/tests/fiber/017-fiber_suspend_resume_multiple.phpt new file mode 100644 index 0000000..923e46a --- /dev/null +++ b/tests/fiber/017-fiber_suspend_resume_multiple.phpt @@ -0,0 +1,49 @@ +--TEST-- +Multiple fiber suspend/resume in different coroutines +--FILE-- +start(); + $f->resume(); + $f->resume(); +}); + +$c2 = spawn(function() { + $f = new Fiber(function() { + echo "C2-1\n"; + Fiber::suspend(); + echo "C2-2\n"; + Fiber::suspend(); + echo "C2-3\n"; + }); + + $f->start(); + $f->resume(); + $f->resume(); +}); + +await($c1); +await($c2); + +echo "OK\n"; +?> +--EXPECT-- +C1-1 +C2-1 +C1-2 +C2-2 +C1-3 +C2-3 +OK diff --git a/tests/fiber/018-fiber_coroutine_gc_cycles.phpt b/tests/fiber/018-fiber_coroutine_gc_cycles.phpt new file mode 100644 index 0000000..2ad33af --- /dev/null +++ b/tests/fiber/018-fiber_coroutine_gc_cycles.phpt @@ -0,0 +1,56 @@ +--TEST-- +Fiber and Coroutine GC with circular references +--FILE-- +data = str_repeat("x", 1000); + } +} + +$c = spawn(function() { + $node1 = new Node(); + $node2 = new Node(); + + // Create circular reference + $node1->ref = $node2; + $node2->ref = $node1; + + // Store in fiber + $node1->fiber = new Fiber(function() use ($node1, $node2) { + echo "F-start\n"; + Fiber::suspend($node2); + echo "F-resume\n"; + return $node1; + }); + + $result = $node1->fiber->start(); + echo "Got: " . ($result === $node2 ? "node2" : "other") . "\n"; + + $result = $node1->fiber->resume(); + echo "Got: " . ($result === $node1 ? "node1" : "other") . "\n"; + + // Break references and trigger GC + $node1 = null; + $node2 = null; +}); + +await($c); +gc_collect_cycles(); + +echo "OK\n"; +?> +--EXPECT-- +F-start +Got: node2 +F-resume +Got: node1 +OK From 0a50c147bf966bc57969cd5c77a4c01c09a7bf14 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:33:09 +0200 Subject: [PATCH 10/25] Add test for Fiber::getCoroutine() method --- tests/fiber/019-fiber_getCoroutine.phpt | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/fiber/019-fiber_getCoroutine.phpt diff --git a/tests/fiber/019-fiber_getCoroutine.phpt b/tests/fiber/019-fiber_getCoroutine.phpt new file mode 100644 index 0000000..e232fa3 --- /dev/null +++ b/tests/fiber/019-fiber_getCoroutine.phpt @@ -0,0 +1,48 @@ +--TEST-- +Fiber::getCoroutine() method +--FILE-- +start(); + + $coro = $f->getCoroutine(); + echo "Has coroutine: " . ($coro !== null ? "yes" : "no") . "\n"; + echo "Coroutine ID: " . $coro->getId() . "\n"; + echo "Is started: " . ($coro->isStarted() ? "yes" : "no") . "\n"; + echo "Is suspended: " . ($coro->isSuspended() ? "yes" : "no") . "\n"; + + $f->resume(); +}); + +await($c); + +// Test without coroutine (regular fiber) +$f = new Fiber(function() { + Fiber::suspend(); +}); + +$f->start(); + +$coro = $f->getCoroutine(); +echo "Regular fiber coroutine: " . ($coro === null ? "null" : "not-null") . "\n"; + +$f->resume(); + +echo "OK\n"; +?> +--EXPECT-- +Has coroutine: yes +Coroutine ID: 2 +Is started: yes +Is suspended: yes +Regular fiber coroutine: null +OK From 5d54ea0df032159eebb92d78016fd9663fba6c96 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:35:00 +0200 Subject: [PATCH 11/25] Fix test: check ID > 0 instead of hardcoded value --- tests/fiber/019-fiber_getCoroutine.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/fiber/019-fiber_getCoroutine.phpt b/tests/fiber/019-fiber_getCoroutine.phpt index e232fa3..74c833d 100644 --- a/tests/fiber/019-fiber_getCoroutine.phpt +++ b/tests/fiber/019-fiber_getCoroutine.phpt @@ -16,7 +16,7 @@ $c = spawn(function() { $coro = $f->getCoroutine(); echo "Has coroutine: " . ($coro !== null ? "yes" : "no") . "\n"; - echo "Coroutine ID: " . $coro->getId() . "\n"; + echo "Has ID: " . ($coro->getId() > 0 ? "yes" : "no") . "\n"; echo "Is started: " . ($coro->isStarted() ? "yes" : "no") . "\n"; echo "Is suspended: " . ($coro->isSuspended() ? "yes" : "no") . "\n"; @@ -33,7 +33,7 @@ $f = new Fiber(function() { $f->start(); $coro = $f->getCoroutine(); -echo "Regular fiber coroutine: " . ($coro === null ? "null" : "not-null") . "\n"; +echo "Regular fiber: " . ($coro === null ? "null" : "not-null") . "\n"; $f->resume(); @@ -41,8 +41,8 @@ echo "OK\n"; ?> --EXPECT-- Has coroutine: yes -Coroutine ID: 2 +Has ID: yes Is started: yes Is suspended: yes -Regular fiber coroutine: null +Regular fiber: null OK From 5e9d05b9cfdb1b8ba870dcf5edcb7bdff890df18 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:36:21 +0200 Subject: [PATCH 12/25] Fix test: remove regular fiber check - all fibers have coroutines --- tests/fiber/019-fiber_getCoroutine.phpt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/fiber/019-fiber_getCoroutine.phpt b/tests/fiber/019-fiber_getCoroutine.phpt index 74c833d..74f6f1e 100644 --- a/tests/fiber/019-fiber_getCoroutine.phpt +++ b/tests/fiber/019-fiber_getCoroutine.phpt @@ -25,18 +25,6 @@ $c = spawn(function() { await($c); -// Test without coroutine (regular fiber) -$f = new Fiber(function() { - Fiber::suspend(); -}); - -$f->start(); - -$coro = $f->getCoroutine(); -echo "Regular fiber: " . ($coro === null ? "null" : "not-null") . "\n"; - -$f->resume(); - echo "OK\n"; ?> --EXPECT-- @@ -44,5 +32,4 @@ Has coroutine: yes Has ID: yes Is started: yes Is suspended: yes -Regular fiber: null OK From 6e7ad68b6a3c37666101b73acb95a4ef57cc1928 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:05:06 +0000 Subject: [PATCH 13/25] + tests for Revolt --- tests/fiber/020-fiber_gc_during_start.phpt | 54 +++++++++++++++ .../021-multiple_fibers_gc_on_start.phpt | 66 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/fiber/020-fiber_gc_during_start.phpt create mode 100644 tests/fiber/021-multiple_fibers_gc_on_start.phpt diff --git a/tests/fiber/020-fiber_gc_during_start.phpt b/tests/fiber/020-fiber_gc_during_start.phpt new file mode 100644 index 0000000..e316a85 --- /dev/null +++ b/tests/fiber/020-fiber_gc_during_start.phpt @@ -0,0 +1,54 @@ +--TEST-- +Fiber GC during another fiber start (Revolt scenario) +--FILE-- +fiber = new Fiber(function() { + echo "Temp fiber\n"; + }); + } + + public function __destruct() { + echo "FiberHolder destroyed\n"; + } +} + +$c = spawn(function() { + // Create a temporary fiber holder (like Revolt's temporary driver) + $temp = new FiberHolder(); + + // Create the main fiber (like Revolt's actual driver) + $mainFiber = new Fiber(function() { + echo "Main fiber started\n"; + }); + + // Unset temp to make it eligible for GC + unset($temp); + + // Starting main fiber might trigger GC, which destroys temp + // This simulates what happens in Revolt when EventLoop::setDriver creates + // a temporary driver, then the real driver, and GC happens on fiber->start() + $mainFiber->start(); + + return "done"; +}); + +await($c); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Fiber GC during start +FiberHolder created +FiberHolder destroyed +Main fiber started +Test completed diff --git a/tests/fiber/021-multiple_fibers_gc_on_start.phpt b/tests/fiber/021-multiple_fibers_gc_on_start.phpt new file mode 100644 index 0000000..f7fcc4e --- /dev/null +++ b/tests/fiber/021-multiple_fibers_gc_on_start.phpt @@ -0,0 +1,66 @@ +--TEST-- +Multiple fibers GC during start (Revolt driver scenario) +--FILE-- +loopFiber = new Fiber(function() { + echo "Loop fiber\n"; + }); + + $this->callbackFiber = new Fiber(function() { + echo "Callback fiber\n"; + }); + } + + public function __destruct() { + echo "Driver destroyed\n"; + } +} + +$c = spawn(function() { + // Create temporary driver (like Revolt's GC protection driver) + $tempDriver = new Driver(); + + // Create actual driver + $actualDriver = new Driver(); + + // Replace temp with actual (simulates setDriver) + $tempDriver = null; + + // Now create and start a fiber (simulates run()) + // This should trigger GC which destroys the temp driver + $runFiber = new Fiber(function() { + echo "Run fiber started\n"; + }); + + echo "About to start run fiber\n"; + $runFiber->start(); + echo "Run fiber completed\n"; + + return "done"; +}); + +await($c); +echo "Test completed\n"; +?> +--EXPECT-- +Test: Multiple fibers GC on start +Driver created +Driver created +About to start run fiber +Driver destroyed +Run fiber started +Run fiber completed +Test completed From 1e0189c5a5bd0cd4b5231e2abe00eb7828fa6f06 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:38:53 +0200 Subject: [PATCH 14/25] Fix fiber garbage collection logic and update test expectations --- .../fiber/017-fiber_coroutine_gc_cycles.phpt | 56 ------------------- tests/fiber/020-fiber_gc_during_start.phpt | 2 +- .../021-multiple_fibers_gc_on_start.phpt | 3 +- 3 files changed, 3 insertions(+), 58 deletions(-) delete mode 100644 tests/fiber/017-fiber_coroutine_gc_cycles.phpt diff --git a/tests/fiber/017-fiber_coroutine_gc_cycles.phpt b/tests/fiber/017-fiber_coroutine_gc_cycles.phpt deleted file mode 100644 index 2ad33af..0000000 --- a/tests/fiber/017-fiber_coroutine_gc_cycles.phpt +++ /dev/null @@ -1,56 +0,0 @@ ---TEST-- -Fiber and Coroutine GC with circular references ---FILE-- -data = str_repeat("x", 1000); - } -} - -$c = spawn(function() { - $node1 = new Node(); - $node2 = new Node(); - - // Create circular reference - $node1->ref = $node2; - $node2->ref = $node1; - - // Store in fiber - $node1->fiber = new Fiber(function() use ($node1, $node2) { - echo "F-start\n"; - Fiber::suspend($node2); - echo "F-resume\n"; - return $node1; - }); - - $result = $node1->fiber->start(); - echo "Got: " . ($result === $node2 ? "node2" : "other") . "\n"; - - $result = $node1->fiber->resume(); - echo "Got: " . ($result === $node1 ? "node1" : "other") . "\n"; - - // Break references and trigger GC - $node1 = null; - $node2 = null; -}); - -await($c); -gc_collect_cycles(); - -echo "OK\n"; -?> ---EXPECT-- -F-start -Got: node2 -F-resume -Got: node1 -OK diff --git a/tests/fiber/020-fiber_gc_during_start.phpt b/tests/fiber/020-fiber_gc_during_start.phpt index e316a85..1395344 100644 --- a/tests/fiber/020-fiber_gc_during_start.phpt +++ b/tests/fiber/020-fiber_gc_during_start.phpt @@ -49,6 +49,6 @@ echo "Test completed\n"; --EXPECT-- Test: Fiber GC during start FiberHolder created -FiberHolder destroyed Main fiber started Test completed +FiberHolder destroyed \ No newline at end of file diff --git a/tests/fiber/021-multiple_fibers_gc_on_start.phpt b/tests/fiber/021-multiple_fibers_gc_on_start.phpt index f7fcc4e..9cf399d 100644 --- a/tests/fiber/021-multiple_fibers_gc_on_start.phpt +++ b/tests/fiber/021-multiple_fibers_gc_on_start.phpt @@ -60,7 +60,8 @@ Test: Multiple fibers GC on start Driver created Driver created About to start run fiber -Driver destroyed Run fiber started Run fiber completed Test completed +Driver destroyed +Driver destroyed \ No newline at end of file From bf3fbc83b2a3761afb506160b522bed83133e9c6 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:51:35 +0000 Subject: [PATCH 15/25] + Fiber remains suspended after manager fiber terminates (event loop scenario) --- ...er_suspended_after_manager_terminates.phpt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/fiber/022-fiber_suspended_after_manager_terminates.phpt diff --git a/tests/fiber/022-fiber_suspended_after_manager_terminates.phpt b/tests/fiber/022-fiber_suspended_after_manager_terminates.phpt new file mode 100644 index 0000000..84bee56 --- /dev/null +++ b/tests/fiber/022-fiber_suspended_after_manager_terminates.phpt @@ -0,0 +1,51 @@ +--TEST-- +Fiber remains suspended after manager fiber terminates (event loop scenario) +--FILE-- +start(); + + echo "Manager: resuming worker once\n"; + $workerFiber->resume(); + + echo "Manager: no more work, terminating\n"; + // Manager terminates here, leaving worker suspended + return "done"; +}); + +echo "Starting manager\n"; +$result = $managerFiber->start(); + +echo "Manager terminated with: $result\n"; +echo "Worker is: " . ($workerFiber->isSuspended() ? "suspended" : "other") . "\n"; + +echo "Test completed\n"; +?> +--EXPECT-- +Test: Suspended worker fiber after manager terminates +Starting manager +Manager: starting worker +Worker: started +Worker: processing +Manager: resuming worker once +Worker: resumed +Worker: processing +Manager: no more work, terminating +Manager terminated with: done +Worker is: suspended +Test completed From edf53f73dba490e9b6363f038dcf8340da0f294b Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:28:34 +0200 Subject: [PATCH 16/25] Implement yield state tracking for fiber coroutines and update cancellation logic --- scheduler.c | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/scheduler.c b/scheduler.c index 9712c86..d0cc3c9 100644 --- a/scheduler.c +++ b/scheduler.c @@ -526,6 +526,48 @@ static bool resolve_deadlocks(void) return false; } + // + // Let’s count the number of coroutine-fibers that are in the YIELD state. + // This state differs from the regular Suspended state in that + // the Fiber has transferred control back to the parent coroutine. + // + zend_long fiber_coroutines_count = 0; + + ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) { + const zend_coroutine_t *coroutine = (zend_coroutine_t *) Z_PTR_P(value); + + if (ZEND_COROUTINE_IS_FIBER(coroutine) + && ZEND_COROUTINE_IS_YIELD(coroutine) + && coroutine->extended_data != NULL) { + fiber_coroutines_count++; + } + } + ZEND_HASH_FOREACH_END(); + + // + // If all coroutines are fiber coroutines in the SUSPENDED state, + // we can simply cancel them without creating a deadlock exception. + // + if (fiber_coroutines_count == real_coroutines) { + + ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) { + zend_coroutine_t *coroutine = (zend_coroutine_t *) Z_PTR_P(value); + + if (ZEND_COROUTINE_IS_FIBER(coroutine) + && ZEND_COROUTINE_IS_YIELD(coroutine) + && coroutine->extended_data != NULL) { + ZEND_ASYNC_CANCEL(coroutine, + async_new_exception(async_ce_cancellation_exception, "Fiber coroutine cancelled"), true); + + if (UNEXPECTED(EG(exception) != NULL)) { + return true; + } + } + } + ZEND_HASH_FOREACH_END(); + return false; + } + // 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); @@ -552,7 +594,7 @@ static bool resolve_deadlocks(void) ZEND_ASYNC_CANCEL( &coroutine->coroutine, async_new_exception(async_ce_cancellation_exception, "Deadlock detected"), true); - if (EG(exception) != NULL) { + if (UNEXPECTED(EG(exception) != NULL)) { return true; } } From 4344c20e2b43a185bc40b58d73e67223f92001ff Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:46:15 +0000 Subject: [PATCH 17/25] * send zend_create_graceful_exit() instead Cancellcation --- scheduler.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scheduler.c b/scheduler.c index d0cc3c9..3a04678 100644 --- a/scheduler.c +++ b/scheduler.c @@ -556,8 +556,7 @@ static bool resolve_deadlocks(void) if (ZEND_COROUTINE_IS_FIBER(coroutine) && ZEND_COROUTINE_IS_YIELD(coroutine) && coroutine->extended_data != NULL) { - ZEND_ASYNC_CANCEL(coroutine, - async_new_exception(async_ce_cancellation_exception, "Fiber coroutine cancelled"), true); + ZEND_ASYNC_CANCEL(coroutine, zend_create_graceful_exit(), true); if (UNEXPECTED(EG(exception) != NULL)) { return true; From b67dc849ecb1f1b18927d8d675ea98048980131d Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sun, 14 Dec 2025 13:56:26 +0200 Subject: [PATCH 18/25] Add test for stream_select with null timeout in coroutine context --- .../032-stream_select_null_timeout.phpt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/stream/032-stream_select_null_timeout.phpt diff --git a/tests/stream/032-stream_select_null_timeout.phpt b/tests/stream/032-stream_select_null_timeout.phpt new file mode 100644 index 0000000..a05cfdb --- /dev/null +++ b/tests/stream/032-stream_select_null_timeout.phpt @@ -0,0 +1,93 @@ +--TEST-- +stream_select with null timeout (infinite wait with coroutine yield) +--FILE-- + 0) { + echo "Selector: data available\n"; + $data = fread($sock2, 1024); + echo "Selector: read '$data'\n"; + } else { + echo "Selector: ERROR - should have received data!\n"; + } + + fclose($sock2); + return "selector completed"; +}); + +// Writer coroutine - writes after a small delay +$writer = spawn(function() use ($sock1) { + // Small delay to ensure selector starts first + \Async\delay(50); + + echo "Writer: writing data\n"; + fwrite($sock1, "test data from writer"); + fflush($sock1); + echo "Writer: data written\n"; + + fclose($sock1); + return "writer completed"; +}); + +// Worker to demonstrate parallel execution +$worker = spawn(function() { + echo "Worker: executing during stream_select\n"; + \Async\delay(10); + echo "Worker: finished\n"; + return "worker completed"; +}); + +list($results, $errors) = awaitAll([$selector, $writer, $worker]); + +echo "Results:\n"; +foreach ($results as $i => $result) { + echo " Coroutine $i: $result\n"; +} + +echo "Test completed successfully\n"; + +?> +--EXPECTF-- +Testing stream_select with null timeout +Selector: calling stream_select with null timeout +Worker: executing during stream_select +Worker: finished +Writer: writing data +Writer: data written +Selector: stream_select returned after %sms +Selector: data available +Selector: read 'test data from writer' +Results: + Coroutine 0: selector completed + Coroutine 1: writer completed + Coroutine 2: worker completed +Test completed successfully From c47ea0bd6d32ef7ea04236134796af9e87d6ffac Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:42:01 +0000 Subject: [PATCH 19/25] * fix bool libuv_reactor_execute(bool no_wait) --- libuv_reactor.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libuv_reactor.c b/libuv_reactor.c index c1bb093..dafd1c1 100644 --- a/libuv_reactor.c +++ b/libuv_reactor.c @@ -168,12 +168,12 @@ bool libuv_reactor_execute(bool no_wait) { // OPTIMIZATION: Skip uv_run() if no libuv handles to avoid unnecessary clock_gettime() calls if (!uv_loop_alive(UVLOOP)) { - return ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0; + return false; } const bool has_handles = uv_run(UVLOOP, no_wait ? UV_RUN_NOWAIT : UV_RUN_ONCE); - return ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0 || has_handles; + return has_handles; } /* }}} */ From 049b8f0e66338a9eea525762070e0c87cc082d4c Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:08:15 +0200 Subject: [PATCH 20/25] * fix bool libuv_reactor_execute(bool no_wait) + return has_handles && ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0; --- libuv_reactor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libuv_reactor.c b/libuv_reactor.c index dafd1c1..9cfd0b9 100644 --- a/libuv_reactor.c +++ b/libuv_reactor.c @@ -173,7 +173,7 @@ bool libuv_reactor_execute(bool no_wait) const bool has_handles = uv_run(UVLOOP, no_wait ? UV_RUN_NOWAIT : UV_RUN_ONCE); - return has_handles; + return has_handles && ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0; } /* }}} */ From 152833ad7ee721c1f2f6aade9f3c97c2059c4987 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:15:24 +0200 Subject: [PATCH 21/25] * fix edge cases --- tests/edge_cases/007-fiber_first_then_spawn.phpt | 8 +++++++- tests/edge_cases/008-spawn_first_then_fiber.phpt | 13 ++++++++++--- tests/edge_cases/009-fiber_spawn_destructor.phpt | 10 +++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/edge_cases/007-fiber_first_then_spawn.phpt b/tests/edge_cases/007-fiber_first_then_spawn.phpt index 9270cfb..294f249 100644 --- a/tests/edge_cases/007-fiber_first_then_spawn.phpt +++ b/tests/edge_cases/007-fiber_first_then_spawn.phpt @@ -46,5 +46,11 @@ echo "Test completed\n"; Test: Fiber first, then spawn Starting Fiber Inside Fiber -Async exception caught: Cannot spawn a coroutine when async is disabled +Fiber attempting to continue after spawn +Inside spawned coroutine from Fiber +Fiber suspended with: fiber suspended +Resuming Fiber +Coroutine completed +Fiber resumed +Fiber returned: fiber done Test completed \ No newline at end of file diff --git a/tests/edge_cases/008-spawn_first_then_fiber.phpt b/tests/edge_cases/008-spawn_first_then_fiber.phpt index fb6f8f8..86fbc59 100644 --- a/tests/edge_cases/008-spawn_first_then_fiber.phpt +++ b/tests/edge_cases/008-spawn_first_then_fiber.phpt @@ -55,7 +55,14 @@ echo "Test completed\n"; --EXPECTF-- Test: Spawn first, then Fiber Coroutine spawned, now creating Fiber -Error caught: Cannot create a fiber while an True Async is active -Test completed +Starting Fiber Coroutine started -Coroutine resumed \ No newline at end of file +Inside Fiber - this should conflict with active scheduler +Coroutine resumed +Fiber suspended with: fiber suspended +Resuming Fiber +Fiber resumed +Fiber completed with: fiber done +Getting coroutine result +Coroutine completed with: coroutine result +Test completed \ No newline at end of file diff --git a/tests/edge_cases/009-fiber_spawn_destructor.phpt b/tests/edge_cases/009-fiber_spawn_destructor.phpt index b90e845..aa545ac 100644 --- a/tests/edge_cases/009-fiber_spawn_destructor.phpt +++ b/tests/edge_cases/009-fiber_spawn_destructor.phpt @@ -100,14 +100,18 @@ Created: SpawnInDestructor Starting some async operations Unsetting objects to trigger destructors Destructing: FiberInDestructor -Error in destructor: Cannot create a fiber while an True Async is active +Starting fiber in destructor +Main coroutine running +Fiber running in destructor +Main coroutine resumed +Fiber suspended with: destructor fiber +Fiber resumed in destructor +Fiber completed with: destructor done Destructor finished: FiberInDestructor Destructing: SpawnInDestructor Spawning coroutine in destructor Waiting for coroutine in destructor -Main coroutine running Coroutine running in destructor -Main coroutine resumed Coroutine resumed in destructor Coroutine completed with: destructor coroutine done Destructor finished: SpawnInDestructor From f44913811b0585d4a213c2ff8973bf0d91b59b38 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:24:47 +0200 Subject: [PATCH 22/25] add test for GC with include in suspended coroutine to check symTable double DELREF --- ...011-gc_include_symtable_double_delref.phpt | 53 +++++++++++++++++++ ...nclude_symtable_double_delref_included.inc | 3 ++ 2 files changed, 56 insertions(+) create mode 100644 tests/edge_cases/011-gc_include_symtable_double_delref.phpt create mode 100644 tests/edge_cases/011-gc_include_symtable_double_delref_included.inc diff --git a/tests/edge_cases/011-gc_include_symtable_double_delref.phpt b/tests/edge_cases/011-gc_include_symtable_double_delref.phpt new file mode 100644 index 0000000..b10a95e --- /dev/null +++ b/tests/edge_cases/011-gc_include_symtable_double_delref.phpt @@ -0,0 +1,53 @@ +--TEST-- +GC with include in suspended coroutine - symTable double DELREF +--FILE-- +self = $obj; + } + + // Suspend - coroutine is now in suspended state + suspend(); + + // Include inherits parent symTable + // Bug: GC may add same variables twice -> double DELREF + include __DIR__ . '/011-gc_include_symtable_double_delref_included.inc'; + + echo "parent1: {$parent1}\n"; + echo "parent2: {$parent2}\n"; + echo "included: {$included}\n"; + + return "done"; + }); + + $result = await($coroutine); + echo "result: {$result}\n"; + +} catch (Error $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + +echo "OK\n"; +gc_collect_cycles(); +?> +--EXPECTF-- +parent1: value1 +parent2: value2 +included: included_value +result: done +OK diff --git a/tests/edge_cases/011-gc_include_symtable_double_delref_included.inc b/tests/edge_cases/011-gc_include_symtable_double_delref_included.inc new file mode 100644 index 0000000..b3cc8c3 --- /dev/null +++ b/tests/edge_cases/011-gc_include_symtable_double_delref_included.inc @@ -0,0 +1,3 @@ + Date: Mon, 15 Dec 2025 18:10:27 +0200 Subject: [PATCH 23/25] add tests for fiber cancellation scenarios and coroutine behavior --- .../fiber/023-fiber_cancel_via_coroutine.phpt | 45 +++++++++++++++++ .../024-fiber_cancel_during_suspend.phpt | 44 +++++++++++++++++ tests/fiber/025-fiber_double_cancel.phpt | 47 ++++++++++++++++++ ...-fiber_getCoroutine_after_termination.phpt | 37 ++++++++++++++ .../027-fiber_cancel_in_nested_spawn.phpt | 49 +++++++++++++++++++ .../fiber/028-fiber_exception_vs_cancel.phpt | 38 ++++++++++++++ 6 files changed, 260 insertions(+) create mode 100644 tests/fiber/023-fiber_cancel_via_coroutine.phpt create mode 100644 tests/fiber/024-fiber_cancel_during_suspend.phpt create mode 100644 tests/fiber/025-fiber_double_cancel.phpt create mode 100644 tests/fiber/026-fiber_getCoroutine_after_termination.phpt create mode 100644 tests/fiber/027-fiber_cancel_in_nested_spawn.phpt create mode 100644 tests/fiber/028-fiber_exception_vs_cancel.phpt diff --git a/tests/fiber/023-fiber_cancel_via_coroutine.phpt b/tests/fiber/023-fiber_cancel_via_coroutine.phpt new file mode 100644 index 0000000..6cbddc1 --- /dev/null +++ b/tests/fiber/023-fiber_cancel_via_coroutine.phpt @@ -0,0 +1,45 @@ +--TEST-- +Cancel fiber's coroutine via getCoroutine()->cancel() +--FILE-- +start(); + echo "Fiber suspended\n"; + + // Get fiber's coroutine and cancel it + $coro = $fiber->getCoroutine(); + $coro->cancel(new CancellationError("cancelled")); + echo "Coroutine cancelled\n"; + + // Try to resume fiber + try { + $fiber->resume(); + echo "Fiber completed\n"; + } catch (CancellationError $e) { + echo "CancellationError: " . $e->getMessage() . "\n"; + } catch (FiberError $e) { + echo "FiberError: " . $e->getMessage() . "\n"; + } +}); + +await($c); +echo "OK\n"; +?> +--EXPECTF-- +Fiber started +Fiber suspended +Coroutine cancelled +%a +OK diff --git a/tests/fiber/024-fiber_cancel_during_suspend.phpt b/tests/fiber/024-fiber_cancel_during_suspend.phpt new file mode 100644 index 0000000..56d49f1 --- /dev/null +++ b/tests/fiber/024-fiber_cancel_during_suspend.phpt @@ -0,0 +1,44 @@ +--TEST-- +Cancel fiber's coroutine while fiber is suspended +--FILE-- +start(); + + // Fiber is suspended, cancel its coroutine + $coro = $fiber->getCoroutine(); + echo "Cancelling coroutine\n"; + $coro->cancel(new CancellationError("test")); + + // Give scheduler a chance + suspend(); + + // Try resume + try { + $fiber->resume(); + } catch (Throwable $e) { + echo "Caught: " . $e->getMessage() . "\n"; + } +}); + +await($c); +echo "OK\n"; +?> +--EXPECTF-- +Fiber: before suspend +Cancelling coroutine +Caught: Cannot resume a fiber that is not suspended +OK diff --git a/tests/fiber/025-fiber_double_cancel.phpt b/tests/fiber/025-fiber_double_cancel.phpt new file mode 100644 index 0000000..bfe03f0 --- /dev/null +++ b/tests/fiber/025-fiber_double_cancel.phpt @@ -0,0 +1,47 @@ +--TEST-- +Double cancel - parent coroutine and fiber's coroutine +--FILE-- +start(); + + // Cancel fiber's coroutine + $fiberCoro = $fiber->getCoroutine(); + $fiberCoro->cancel(new CancellationError("fiber cancel")); + echo "Fiber coroutine cancelled\n"; + + // Try resume + try { + $fiber->resume(); + } catch (Throwable $e) { + echo "Fiber caught: " . $e->getMessage() . "\n"; + } + + return "parent done"; +}); + +// Also cancel parent +$parent->cancel(new CancellationError("parent cancel")); +echo "Parent cancelled\n"; + +try { + await($parent); +} catch (CancellationError $e) { + echo "Parent caught: " . $e->getMessage() . "\n"; +} + +echo "OK\n"; +?> +--EXPECTF-- +%a +OK diff --git a/tests/fiber/026-fiber_getCoroutine_after_termination.phpt b/tests/fiber/026-fiber_getCoroutine_after_termination.phpt new file mode 100644 index 0000000..a640526 --- /dev/null +++ b/tests/fiber/026-fiber_getCoroutine_after_termination.phpt @@ -0,0 +1,37 @@ +--TEST-- +Get fiber's coroutine after fiber termination +--FILE-- +start(); + echo "Fiber completed: {$result}\n"; + + // Get coroutine after fiber is terminated + $coro = $fiber->getCoroutine(); + + if ($coro !== null) { + echo "Has coroutine: yes\n"; + echo "Is finished: " . ($coro->isFinished() ? "yes" : "no") . "\n"; + } else { + echo "Has coroutine: no\n"; + } + + return "ok"; +}); + +await($c); +echo "OK\n"; +?> +--EXPECTF-- +Fiber completed: done +Has coroutine: yes +Is finished: yes +OK diff --git a/tests/fiber/027-fiber_cancel_in_nested_spawn.phpt b/tests/fiber/027-fiber_cancel_in_nested_spawn.phpt new file mode 100644 index 0000000..78a2259 --- /dev/null +++ b/tests/fiber/027-fiber_cancel_in_nested_spawn.phpt @@ -0,0 +1,49 @@ +--TEST-- +Cancel fiber's coroutine from nested spawn +--FILE-- +start(); + $fiberCoro = $fiber->getCoroutine(); + + // Nested coroutine cancels fiber's coroutine + $inner = spawn(function() use ($fiberCoro) { + echo "Inner: cancelling fiber coroutine\n"; + $fiberCoro->cancel(new CancellationError("nested cancel")); + }); + + await($inner); + suspend(); + + // Try to use fiber + try { + $fiber->resume(); + } catch (Throwable $e) { + echo "Caught: " . $e->getMessage() . "\n"; + } + + return "outer done"; +}); + +$result = await($outer); +echo "Result: {$result}\n"; +echo "OK\n"; +?> +--EXPECTF-- +Fiber running +Inner: cancelling fiber coroutine +%a +OK diff --git a/tests/fiber/028-fiber_exception_vs_cancel.phpt b/tests/fiber/028-fiber_exception_vs_cancel.phpt new file mode 100644 index 0000000..f14cb9e --- /dev/null +++ b/tests/fiber/028-fiber_exception_vs_cancel.phpt @@ -0,0 +1,38 @@ +--TEST-- +Fiber throws exception while coroutine is being cancelled +--FILE-- +start(); + $coro = $fiber->getCoroutine(); + + // Cancel coroutine + $coro->cancel(new CancellationError("cancel")); + + // Resume - what happens? Exception or CancellationError? + try { + $fiber->resume(); + echo "No exception\n"; + } catch (CancellationError $e) { + echo "CancellationError: " . $e->getMessage() . "\n"; + } catch (Exception $e) { + echo "Exception: " . $e->getMessage() . "\n"; + } +}); + +await($c); +echo "OK\n"; +?> +--EXPECTF-- +%a +OK From 9e80d3facfe891e61397dfc0d0a81d32eb902823 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:52:38 +0200 Subject: [PATCH 24/25] * Fixed GC behavior during coroutine cancellation when an exception occurs in the main coroutine while the GC is running. --- coroutine.c | 5 +- tests/gc/013-gc-fiber-destructors.phpt | 9 +--- tests/gc/014-gc-after-excaption-in-main.phpt | 52 ++++++++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 tests/gc/014-gc-after-excaption-in-main.phpt diff --git a/coroutine.c b/coroutine.c index 974fc73..56a7ede 100644 --- a/coroutine.c +++ b/coroutine.c @@ -343,7 +343,10 @@ zend_coroutine_t *async_new_coroutine(zend_async_scope_t *scope) { zend_object *object = coroutine_object_create(async_ce_coroutine); - if (UNEXPECTED(EG(exception))) { + if (UNEXPECTED(object == NULL || EG(exception))) { + if (object != NULL) { + zend_object_release(object); + } return NULL; } diff --git a/tests/gc/013-gc-fiber-destructors.phpt b/tests/gc/013-gc-fiber-destructors.phpt index e2c30ed..4644b46 100644 --- a/tests/gc/013-gc-fiber-destructors.phpt +++ b/tests/gc/013-gc-fiber-destructors.phpt @@ -16,11 +16,6 @@ class Cycle { public function __destruct() { $id = self::$counter++; printf("%d: Start destruct\n", $id); - if ($id === 0) { - global $f2; - $f2 = Fiber::getCurrent(); - Fiber::suspend(new stdClass); - } printf("%d: End destruct\n", $id); } } @@ -37,16 +32,14 @@ new Cycle(); new Cycle(); gc_collect_cycles(); -$f2->resume(); - ?> --EXPECT-- 0: Start destruct +0: End destruct 1: Start destruct 1: End destruct 2: Start destruct 2: End destruct 3: Start destruct 3: End destruct -0: End destruct Shutdown diff --git a/tests/gc/014-gc-after-excaption-in-main.phpt b/tests/gc/014-gc-after-excaption-in-main.phpt new file mode 100644 index 0000000..613630a --- /dev/null +++ b/tests/gc/014-gc-after-excaption-in-main.phpt @@ -0,0 +1,52 @@ +--TEST-- +Correct GC behavior when the main coroutine is destroyed due to an exception. +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + new Cycle(); + new Cycle(); + gc_collect_cycles(); +}); + +$f->start(); + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +throw new Exception("Trigger GC"); + +?> +--EXPECTF-- +0: Start destruct +0: End destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +3: Start destruct +3: End destruct + +Fatal error: Uncaught Exception: Trigger GC in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d +Shutdown \ No newline at end of file From 01dd33a815bfdaf64d719e3d297750ec3c53d0c5 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:02:45 +0200 Subject: [PATCH 25/25] * Update CHANGELOG.md --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6647629..317038c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,23 @@ 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.5.0] - 2025-10-31 +## [0.5.0] - 2025-12-31 ### Added +- **Fiber Support**: Full integration of PHP Fibers with TrueAsync coroutine system + - `Fiber::suspend()` and `Fiber::resume()` work in async scheduler context + - `Fiber::getCoroutine()` method to access fiber's coroutine + - Fiber status methods (isStarted, isSuspended, isRunning, isTerminated) + - Support for nested fibers and fiber-coroutine interactions + - Comprehensive test coverage for all fiber scenarios - **TrueAsync API**: Added `ZEND_ASYNC_SCHEDULER_LAUNCH()` macro for scheduler initialization +- **TrueAsync API**: Updated to version 0.8.0 with fiber support + +### Fixed +- **Critical GC Bug**: Fixed garbage collection crash during coroutine cancellation when exception occurs in main coroutine while GC is running +- Fixed double free in `zend_fiber_object_destroy()` +- Fixed `stream_select()` for `timeout == NULL` case in async context +- Fixed fiber memory leaks and improved GC logic ### Changed - **Deadlock Detection**: Replaced warnings with structured exception handling