Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b44b96a
Refactor coroutine header includes: update fiber and async API depend…
EdmondDantes Dec 12, 2025
f26fbe2
Add basic fiber creation and execution test with active async scheduler
EdmondDantes Dec 12, 2025
b2e55d5
Add tests for fiber execution: simple return and suspend/resume cycles
EdmondDantes Dec 12, 2025
9438aea
Add tests for multiple suspend/resume cycles and nested fibers
EdmondDantes Dec 12, 2025
d99e1fb
Add tests for Fiber exception handling and propagation
EdmondDantes Dec 12, 2025
d5e7812
+ Fiber status methods
EdmondDantes Dec 12, 2025
b7d8bff
Add tests for Fiber methods: getReturn, garbage collection, and handl…
EdmondDantes Dec 12, 2025
e73a1a6
Refactor Fiber garbage collection and NULL value handling
EdmondDantes Dec 12, 2025
f1736c2
Add fiber tests for independent coroutines
EdmondDantes Dec 13, 2025
0a50c14
Add test for Fiber::getCoroutine() method
EdmondDantes Dec 13, 2025
5d54ea0
Fix test: check ID > 0 instead of hardcoded value
EdmondDantes Dec 13, 2025
5e9d05b
Fix test: remove regular fiber check - all fibers have coroutines
EdmondDantes Dec 13, 2025
6e7ad68
+ tests for Revolt
EdmondDantes Dec 13, 2025
1e0189c
Fix fiber garbage collection logic and update test expectations
EdmondDantes Dec 13, 2025
bf3fbc8
+ Fiber remains suspended after manager fiber terminates (event loop …
EdmondDantes Dec 13, 2025
edf53f7
Implement yield state tracking for fiber coroutines and update cancel…
EdmondDantes Dec 14, 2025
4344c20
* send zend_create_graceful_exit() instead Cancellcation
EdmondDantes Dec 14, 2025
b67dc84
Add test for stream_select with null timeout in coroutine context
EdmondDantes Dec 14, 2025
c47ea0b
* fix bool libuv_reactor_execute(bool no_wait)
EdmondDantes Dec 15, 2025
049b8f0
* fix bool libuv_reactor_execute(bool no_wait) + return has_handles &…
EdmondDantes Dec 15, 2025
152833a
* fix edge cases
EdmondDantes Dec 15, 2025
f449138
add test for GC with include in suspended coroutine to check symTable…
EdmondDantes Dec 15, 2025
5dad0ed
add tests for fiber cancellation scenarios and coroutine behavior
EdmondDantes Dec 15, 2025
9e80d3f
* Fixed GC behavior during coroutine cancellation when an exception o…
EdmondDantes Dec 16, 2025
01dd33a
* Update CHANGELOG.md
EdmondDantes Dec 16, 2025
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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion coroutine.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions coroutine.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#define COROUTINE_H

#include "php_async_api.h"
#include <Zend/zend_fibers.h>
#include <Zend/zend_async_API.h>

/* Fiber context structure for pooling */
Expand Down
4 changes: 2 additions & 2 deletions libuv_reactor.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 && ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0;
}

/* }}} */
Expand Down
55 changes: 52 additions & 3 deletions scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,47 @@ 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, zend_create_graceful_exit(), 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);
Expand All @@ -552,7 +593,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;
}
}
Expand Down Expand Up @@ -1025,6 +1066,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");
Expand All @@ -1033,8 +1084,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;
Expand Down
8 changes: 7 additions & 1 deletion tests/edge_cases/007-fiber_first_then_spawn.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 10 additions & 3 deletions tests/edge_cases/008-spawn_first_then_fiber.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
10 changes: 7 additions & 3 deletions tests/edge_cases/009-fiber_spawn_destructor.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions tests/edge_cases/011-gc_include_symtable_double_delref.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
GC with include in suspended coroutine - symTable double DELREF
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;
use function Async\await;

class Cycle {
public $self;
}

try {
$coroutine = spawn(function() {
$parent1 = "value1";
$parent2 = "value2";

for ($i = 0; $i < 10000; $i++) {
$obj = new Cycle();
$obj->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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
// Included file for GC test
$included = "included_value";
39 changes: 39 additions & 0 deletions tests/fiber/001-fiber_with_coroutine_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Fiber with coroutine: Basic fiber creation and execution when async is active
--FILE--
<?php

use function Async\spawn;
use function Async\await;

echo "Test: Fiber creation with active async scheduler\n";

$coroutine = spawn(function() {
echo "Coroutine started\n";

// Create a fiber while async scheduler is active
$fiber = new Fiber(function() {
echo "Fiber executing\n";
return "fiber result";
});

echo "Starting fiber\n";
$result = $fiber->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
30 changes: 30 additions & 0 deletions tests/fiber/002-fiber_simple_return.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--TEST--
Fiber with simple return without suspend
--FILE--
<?php

use function Async\spawn;
use function Async\await;

echo "Test: Fiber simple return without suspend\n";

$coroutine = spawn(function() {
$fiber = new Fiber(function() {
echo "Fiber executing\n";
return "result from fiber";
});

$result = $fiber->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
37 changes: 37 additions & 0 deletions tests/fiber/003-fiber_one_suspend_resume.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
One suspend/resume cycle
--FILE--
<?php

use function Async\spawn;
use function Async\await;

echo "Test: One suspend/resume cycle\n";

$coroutine = spawn(function() {
$fiber = new Fiber(function() {
echo "Before suspend\n";
$value = Fiber::suspend("suspended");
echo "After resume, got: " . $value . "\n";
return "done";
});

$suspended = $fiber->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
Loading
Loading