Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
782c639
#19: Timer-based test stabilized. Valgrind added for tests.
EdmondDantes Jul 3, 2025
f636253
#19: Timer-based test stabilized. Valgrind added for tests.
EdmondDantes Jul 3, 2025
c26b1a5
#19: * Fixed memory leaks for await tests
EdmondDantes Jul 4, 2025
e207301
#19: * Test 002-proc_open_multiple was stabilized regarding the proce…
EdmondDantes Jul 4, 2025
d850e34
#19: * fix spawn tests
EdmondDantes Jul 4, 2025
1c27de0
#19: - config.m4: require libuv ≥1.44.0 with descriptive error message
EdmondDantes Jul 4, 2025
b7cd677
#19: update pipeline
EdmondDantes Jul 4, 2025
df0b795
#19: update pipeline
EdmondDantes Jul 4, 2025
c7194d6
#19: update pipeline
EdmondDantes Jul 4, 2025
ed314db
#19: update pipeline2
EdmondDantes Jul 4, 2025
34e87c0
#19: update pipeline3
EdmondDantes Jul 4, 2025
0467614
#19: * Memory leaks have been fixed in the await operator and its der…
EdmondDantes Jul 4, 2025
f3a123c
#19: * The ref_count logic for event callbacks has been improved. Spe…
EdmondDantes Jul 4, 2025
b4d4a2d
#19: * Test stabilization awaitAnyOfWithErrors() - basic usage with m…
EdmondDantes Jul 4, 2025
99002bb
#19: * * Test stabilization awaitAnyOfWithErrors() - all coroutines s…
EdmondDantes Jul 4, 2025
bc274d6
#19: % update pipe
EdmondDantes Jul 4, 2025
de8b026
#19: exec\007-exec_vs_shell_exec_comparison.phpt - Test stabilization
EdmondDantes Jul 4, 2025
b6c206d
#19: Fix race conditions and conflicts when running async tests with …
EdmondDantes Jul 4, 2025
e7243ef
#19: fix Server preg
EdmondDantes Jul 4, 2025
2b374ba
#19: fix Http-Server2
EdmondDantes Jul 4, 2025
c4e4e98
#19: await/018-awaitAll_double_free.phpt - test stabilization
EdmondDantes Jul 4, 2025
34efc05
#19: 014-awaitAnyOf_basic.php - test stabilization
EdmondDantes Jul 4, 2025
172df41
#19: awaitAny() - coroutine throws exception - test stabilization
EdmondDantes Jul 4, 2025
7dde7cf
#19: remove Valgrid
EdmondDantes Jul 4, 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
27 changes: 23 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,26 @@ jobs:
libxml2-dev libxslt1-dev libpq-dev libreadline-dev libldap2-dev libsodium-dev \
libargon2-dev \
firebird-dev \
libuv1-dev
valgrind cmake

- name: Install LibUV >= 1.44.0
run: |
# Check if system libuv meets requirements
if pkg-config --exists libuv && pkg-config --atleast-version=1.44.0 libuv; then
echo "System libuv version: $(pkg-config --modversion libuv)"
sudo apt-get install -y libuv1-dev
else
echo "Installing LibUV 1.44.0 from source"
wget https://github.com/libuv/libuv/archive/v1.44.0.tar.gz
tar -xzf v1.44.0.tar.gz
cd libuv-1.44.0
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
sudo ldconfig
cd ../..
fi

- name: Configure PHP
working-directory: php-src
Expand Down Expand Up @@ -140,11 +159,11 @@ jobs:
-d opcache.jit=tracing \
-d zend_test.observer.enabled=1 \
-d zend_test.observer.show_output=0 \
-P -q -x -j2 \
-P -q -x -j4 \
-g FAIL,BORK,LEAK,XLEAK \
--no-progress \
--offline \
--show-diff \
--show-slow 1000 \
--show-slow 2000 \
--set-timeout 120 \
--repeat 2
--repeat 2
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Memory management improvements for long-running async applications
- Proper cleanup of coroutine and scope objects during garbage collection cycles

## [0.2.0] - TBD
### Changed
- **LibUV requirement increased to ≥ 1.44.0** - Requires libuv version 1.44.0 or later to ensure proper UV_RUN_ONCE behavior and prevent busy loop issues that could cause high CPU usage


## [0.2.0] - 2025-07-01

### Added
- **Async-aware destructor handling (PHP Core)**: Implemented `async_shutdown_destructors()` function to properly
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ tightly integrated at the core level.
PHP TRUE ASYNC is supported for PHP 8.5.0 and later.
`LibUV` is the primary reactor implementation for this extension.

### Requirements

- **PHP 8.5.0+**
- **LibUV ≥ 1.44.0** (required) - Fixes critical `UV_RUN_ONCE` busy loop issue that could cause high CPU usage

### Why LibUV 1.44.0+ is Required

Prior to libuv 1.44, there was a critical issue in `uv__io_poll()`/`uv__run_pending` logic that could cause the event loop to "stick" after the first callback when running in `UV_RUN_ONCE` mode, especially when new ready events appeared within callbacks. This resulted in:

- **High CPU usage** due to busy loops
- **Performance degradation** in async applications
- **Inconsistent event loop behavior** affecting TrueAsync API reliability

The fix in libuv 1.44 ensures that `UV_RUN_ONCE` properly returns after processing all ready callbacks in the current iteration, meeting the "forward progress" specification requirements. This is essential for TrueAsync's performance and reliability.

---

### Unix / macOS
Expand Down Expand Up @@ -67,7 +82,31 @@ PHP TRUE ASYNC is supported for PHP 8.5.0 and later.

4. **Install LibUV:**:

Please see the [LibUV installation guide](https://github.com/libuv/libuv)
**IMPORTANT:** LibUV version 1.44.0 or later is required.

For Debian/Ubuntu:
```bash
# Check if system libuv meets requirements (≥1.44.0)
pkg-config --modversion libuv

# If version is too old, install from source:
wget https://github.com/libuv/libuv/archive/v1.44.0.tar.gz
tar -xzf v1.44.0.tar.gz
cd libuv-1.44.0
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
sudo ldconfig
```

For macOS:
```bash
# Homebrew usually has recent versions
brew install libuv
```

Please see the [LibUV installation guide](https://github.com/libuv/libuv) for more details.

5. **Configure and build:**

Expand Down
15 changes: 12 additions & 3 deletions async_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,10 @@ void async_waiting_callback_dispose(zend_async_event_callback_t *callback, zend_

await_callback->await_context = NULL;

if (await_context == NULL) {
return;
if (await_context != NULL) {
await_context->dtor(await_context);
}

await_context->dtor(await_context);
await_callback->prev_dispose(callback, event);
}

Expand All @@ -322,7 +321,9 @@ void async_waiting_callback(
// remove the callback from the 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);
ZEND_ASYNC_EVENT_CALLBACK_DEC_REF(callback);

if (exception != NULL) {
ZEND_ASYNC_EVENT_SET_EXCEPTION_HANDLED(event);
Expand Down Expand Up @@ -356,6 +357,7 @@ void async_waiting_callback(
false
);

callback->dispose(callback, NULL);
return;
}

Expand All @@ -366,6 +368,7 @@ void async_waiting_callback(
ZEND_ASYNC_RESUME(await_callback->callback.coroutine);
}

callback->dispose(callback, NULL);
return;
}

Expand Down Expand Up @@ -393,6 +396,8 @@ void async_waiting_callback(
if (UNEXPECTED(ITERATOR_IS_FINISHED(await_context))) {
ZEND_ASYNC_RESUME(await_callback->callback.coroutine);
}

callback->dispose(callback, NULL);
}

/**
Expand All @@ -415,7 +420,9 @@ void async_waiting_cancellation_callback(
async_await_context_t * await_context = await_callback->await_context;

await_context->resolved_count++;
ZEND_ASYNC_EVENT_CALLBACK_ADD_REF(callback);
event->del_callback(event, callback);
ZEND_ASYNC_EVENT_CALLBACK_DEC_REF(callback);

if (exception != NULL) {
ZEND_ASYNC_EVENT_SET_EXCEPTION_HANDLED(event);
Expand Down Expand Up @@ -445,6 +452,8 @@ void async_waiting_cancellation_callback(
if (await_context->total != 0 && await_context->resolved_count >= await_context->total) {
ZEND_ASYNC_RESUME(await_callback->callback.coroutine);
}

callback->dispose(callback, NULL);
}

zend_result await_iterator_handler(async_iterator_t *iterator, zval *current, zval *key)
Expand Down
5 changes: 3 additions & 2 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ if test "$PHP_ASYNC" = "yes"; then
AC_MSG_CHECKING(for libuv)

if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libuv; then
if $PKG_CONFIG libuv --atleast-version 1.40.0; then
dnl Require libuv >= 1.44.0 for UV_RUN_ONCE busy loop fix
if $PKG_CONFIG libuv --atleast-version 1.44.0; then
LIBUV_INCLINE=`$PKG_CONFIG libuv --cflags`
LIBUV_LIBLINE=`$PKG_CONFIG libuv --libs`
LIBUV_VERSION=`$PKG_CONFIG libuv --modversion`
AC_MSG_RESULT(from pkgconfig: found version $LIBUV_VERSION)
AC_DEFINE(PHP_ASYNC_LIBUV,1,[ ])
else
AC_MSG_ERROR(system libuv must be upgraded to version >= 1.40.0)
AC_MSG_ERROR(system libuv must be upgraded to version >= 1.44.0 (fixes UV_RUN_ONCE busy loop issue))
fi
PHP_EVAL_LIBLINE($LIBUV_LIBLINE, UV_SHARED_LIBADD)
PHP_EVAL_INCLINE($LIBUV_INCLINE)
Expand Down
6 changes: 5 additions & 1 deletion config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ if (PHP_ASYNC == "yes") {
if (CHECK_HEADER_ADD_INCLUDE("libuv/uv.h", "CFLAGS_UV", PHP_PHP_BUILD + "\\include")
&& CHECK_LIB("libuv.lib", "libuv")) {

// Note: libuv >= 1.44.0 is required for UV_RUN_ONCE busy loop fix
// For Windows builds, manually verify libuv version meets requirements

PHP_INSTALL_HEADERS("ext/async", "libuv_reactor.h");

ADD_SOURCES("ext/async", "libuv_reactor.c");
Expand All @@ -39,6 +42,7 @@ if (PHP_ASYNC == "yes") {
ERROR("Libuv components are not found. The search was performed in the directory: '" + PHP_PHP_BUILD +
"'.\nTo compile PHP TRUE ASYNC with LibUV:\n" +
"1. Copy files from 'libuv\\include' to '" + PHP_PHP_BUILD + "\\include\\libuv\\'\n" +
"2. Build libuv.lib and copy it to '" + PHP_PHP_BUILD + "\\lib\\'");
"2. Build libuv.lib and copy it to '" + PHP_PHP_BUILD + "\\lib\\'\n" +
"3. IMPORTANT: Use libuv >= 1.44.0 (fixes UV_RUN_ONCE busy loop issue)");
}
}
3 changes: 2 additions & 1 deletion run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
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"

if [ -z "$1" ]; then
TEST_PATH="$BASE_PATH"
else
TEST_PATH="$BASE_PATH/$1"
fi

"$PHP_EXECUTABLE" "$RUN_TESTS_PATH" --show-diff -p "$PHP_EXECUTABLE" "$TEST_PATH"
"$PHP_EXECUTABLE" "$RUN_TESTS_PATH" --show-diff -m -p "$PHP_EXECUTABLE" "$TEST_PATH"
6 changes: 4 additions & 2 deletions tests/await/006-awaitAny_empty.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ use function Async\awaitAny;
echo "start\n";

$result = awaitAny([]);
var_dump($result);

$resultCheck = $result === null ? "OK" : "FALSE: " . var_export($result, true);
echo "Result is null: $resultCheck\n";

echo "end\n";
?>
--EXPECT--
start
NULL
Result is null: OK
end
3 changes: 2 additions & 1 deletion tests/await/007-awaitAny_exception.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ awaitAny() - coroutine throws exception
use function Async\spawn;
use function Async\awaitAny;
use function Async\delay;
use function Async\suspend;

echo "start\n";

Expand All @@ -15,7 +16,7 @@ $coroutines = [
return "first";
}),
spawn(function() {
delay(20);
suspend();
throw new RuntimeException("test exception");
}),
];
Expand Down
30 changes: 2 additions & 28 deletions tests/await/008-awaitFirstSuccess_basic.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,10 @@ $coroutines = [
];

$result = awaitFirstSuccess($coroutines);
var_dump($result);

echo "Result: {$result[0]}\n";
echo "end\n";
?>
--EXPECTF--
start
array(2) {
[0]=>
string(7) "success"
[1]=>
array(1) {
[0]=>
object(RuntimeException)#%d (7) {
["message":protected]=>
string(11) "first error"
["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
}
}
}
Result: success
end
11 changes: 4 additions & 7 deletions tests/await/014-awaitAnyOf_basic.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,13 @@ $coroutines = [
];

$results = awaitAnyOf(2, $coroutines);
var_dump($results);

$countOfResults = count($results) >= 2 ? "OK" : "FALSE: ".count($results);
echo "Count of results: $countOfResults\n";

echo "end\n";
?>
--EXPECT--
start
array(2) {
[1]=>
string(6) "second"
[3]=>
string(6) "fourth"
}
Count of results: OK
end
40 changes: 8 additions & 32 deletions tests/await/016-awaitAnyOfWithErrors_basic.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,17 @@ $coroutines = [
];

$result = awaitAnyOfWithErrors(2, $coroutines);
var_dump($result);

$countOfResults = count($result[0]) >= 2 ? "OK" : "FALSE: ".count($result[0]);
$countOfErrors = count($result[1]) == 1 ? "OK" : "FALSE: ".count($result[1]);

echo "Count of results: $countOfResults\n";
echo "Count of errors: $countOfErrors\n";

echo "end\n";
?>
--EXPECTF--
start
array(2) {
[0]=>
array(2) {
[2]=>
string(5) "third"
[3]=>
string(6) "fourth"
}
[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
}
}
}
Count of results: OK
Count of errors: OK
end
Loading