Skip to content

Releases: true-async/php-async

v0.7.2

12 Jun 13:09

Choose a tag to compare

What's changed

Thread transfer — closure scope fix

  • fix(#161): closures transferred via spawn_thread / ThreadPool / ThreadChannel now carry their class scope — a static closure declared inside a class arrived in the worker without a scope, so self::/static:: threw Cannot access "self" when no class scope is active; a $this-bound closure resolved Z_OBJCE($this) instead of its declaring class, breaking self:: and private-member visibility under inheritance. The snapshot now carries scope/called_scope by name and re-resolves them in the target thread. A missing class throws Cannot restore closure scope: class "X" not found in the target thread. Closures scoped to anonymous classes are rejected at transfer time.

ext/curl — libcurl 8.20.x DNS hang fix

  • fix(curl): async DNS hang on libcurl 8.20.x — libcurl 8.20.0 moved to a thread-pool DNS resolver whose completion socketpair is not surfaced via CURLMOPT_SOCKETFUNCTION, so the async curl integration (which drives the multi handle purely through curl_multi_socket_action()) never received the resolve result — running_handles stayed pinned and DNS timed out with CURLE_OPERATION_TIMEDOUT. Fixed by calling curl_multi_perform() in both timer callbacks to drain resolver results. The workaround is #if-scoped to >= 8.20.0 && < 8.21.0 only — it compiles out on 8.19 and earlier (native socketpair path) and on 8.21+, where libcurl fixes this upstream (curl/curl#21476).

Requires

  • php-src php-8.6.0-trueasync-0.7.2 (libcurl DNS fix in ext/curl)

v0.7.1

09 Jun 04:53

Choose a tag to compare

What's changed

ThreadPool — memory safety & correctness fixes

  • fix(threadpool): per-task nursery scope for sync tasks — snapshot UAF fixed: sync task body now runs in its own nursery Scope; spawned coroutines are drained before the snapshot arena is freed (Windows debug-heap crash / ASAN-caught on Linux)
  • fix(threadpool): deliver fatal in sync task body to awaiter — a fatal (OOM/exit) no longer hangs the awaiter; future is rejected with ThreadTransferException
  • fix(threadpool): fatal cause delivery — coroutine-mode tasks no longer silently resolve to null on a fatal; cause is built on the healthy parent side from a pestrdup'd message (not from the dying worker's allocator)
  • fix(threadpool): snapshot UAF + libuv loop leak on fatal — op_array name strings materialized into refcounted heap strings; uv_async handles (ThreadChannel/slot_event) now properly disposed on bailout through SUSPEND

Windows fixes

  • fix(win): sendfile to socket via TransmitFileuv_fs_sendfile to a TCP socket was broken on Windows (Winsock SOCKET ≠ CRT fd); replaced with TransmitFile

Cross-thread memory safety

  • fix: closure captured-variable names UAF in spawn_thread — interned string keys from parent's table were stored in worker snapshot; freed when parent ended; now copied into private persistent strings

Test stabilisation

  • Stabilised two race-flaky tests surfaced by CI on Windows (063-bootloader_exception, 034-stdio_fopen_fwrite)

Requires

  • php-src ABI v0.20.0 (zend_async_scope_await_after_cancellation_fn) — ship with php-8.6.0-trueasync-0.7.1

v0.7.0 — ThreadPool, request scope, stability hardening

02 Jun 15:38

Choose a tag to compare

First stable release since v0.6.7 — a large capability + hardening release that folds in the entire 0.7.0 alpha/beta/rc cycle. Headlines: a real OS-thread ThreadPool (per-worker bootloaders, coroutine-mode tasks, thread channels), request-level scope (Async\request_context()), opt-in PDO prepared-statement pooling (~2.9×), three-layer channel/pool deadlock protection, and a deep stability pass driven by a new randomized chaos test suite (now 100% public-API coverage). ABI bumped to v0.19.0.

This runtime powers the high-performance HTTP/1.1 · HTTP/2 · HTTP/3 application server true-async/server, released in lockstep as v0.7.2.

⚠️ Breaking changes

  • new Scope() now defaults to Not-Safe disposal. A fresh root Scope no longer sets DISPOSE_SAFELY: dispose() cancels its coroutines synchronously instead of leaving zombies. Main scope and Scope::inherit(...) chains are unchanged. Migration: (new Scope())->allowZombies() to keep the old behavior.
  • For API/reactor consumers: ABI 0.15 → 0.19 (unified thread-pool factory; zend_async_io_register gains sendfile_fn/fs_open_fn; io_closed field on IO/UDP reqs); TaskGroup/TaskSet seal()close(), isSealed()isClosed(); cross-thread transfer now rejects closures that declare classes/functions (file:line).

Added — multithreading

  • Async\ThreadPool — pool of OS threads for PHP closures: submit()/map()/close() (graceful)/cancel() (rejects backlog), counters, Countable; ThreadPoolException when closed.
  • Workers auto-detect (workers: 0available_parallelism()), per-worker bootloader hook, and coroutine: true mode (each task is a coroutine in its own child scope — may await/use channels/IO). cancel() in coroutine mode actually kills in-flight tasks.
  • Async\ThreadChannel — thread-safe channel via deep-copy snapshot; send/recv suspend the coroutine, not the OS thread (ThreadChannelException).
  • C-only ThreadPool::submit_internal; cross-thread top-level zval transfer helpers.

Added — concurrency & pooling

  • Request-level scopeAsync\request_context(): ?Context, O(1) ZEND_ASYNC_REQUEST_SCOPE (#105). This is what the true-async/server HttpServerConfig::setRequestScope() knob builds on.
  • Channel deadlock protection (3 layers): per-channel noProducerTimeout/noConsumerTimeout, global soft-timer resolver, owner-scope auto-close; typed Async\ChannelCloseReason.
  • PDO Pool prepared-statement cachePDO::ATTR_POOL_STMT_CACHE_SIZE (pgsql/mysql/sqlite), per-conn LRU with transparent plan-invalidation retry; ~2.9× on a tight prepare+execute+fetch loop.
  • PDO_SQLite connection pool (PDO::ATTR_POOL_ENABLED).
  • TaskGroup/TaskSet queueLimit backpressure (default 2 × concurrency).
  • Timer rearm / multishot API.

Added — I/O & introspection

  • Async sendfile (uv_fs_sendfile) and async open(2).
  • Async\available_parallelism() (respects cgroup quota/affinity); CPU probesCpuSnapshot::now(), cpu_usage(), loadavg() (ZTS-safe; null on Windows where N/A).

Changed

  • TaskGroup/TaskSet seal()close() terminology.
  • fuzzy_tests/fuzzy-tests/.

Performance

  • +32% RPS on a minimal HTTP handler — static TSRMLS cache (ZEND_ENABLE_STATIC_TSRMLS_CACHE) turns every EG()/ASYNC_G() into a single __thread load.

Fixed

Deep stability pass (full per-bug detail in CHANGELOG.md):

  • Thread pool / cross-thread transfer — worker crash on exit()/die() in task/bootloader (#154), bootloader-error swallowing (#154), cross-thread task UAF under cancel-vs-blocked-worker (#146), $this-bound closure SEGV, enum singleton identity, self-referential object cycles, type-info deep-copy (~12k→175k req/s under load).
  • Reactor / I/O — close-mid-read hangs parked reader (#144), concurrent writes to one file/descriptor losing data or corrupting heap on macOS/Windows (#129), writer not waking on peer reset, double-stop event underflow, Windows TCP accept.
  • Channels / poolsChannel(0) close-vs-send split-brain (#127) and missing rendezvous (#108), pool deadlock on broken-release (#141), recvAsync()/pool GC-cycle leaks.
  • curlcurl_multi_select cancel UAF (#145), async-read dangling subscription, callback exception leaks (#118).
  • JIT — tracing-JIT stale-FP spill SEGV in chaos fuzz (#118).
  • Leaks — OpenSSL 3 per-thread RCU state, scope/timer/signal/composite-exception refcounts, OOM-bailout double-warning.
  • Late await() double-throw on a coroutine that finished with an exception (#139); Async\iterate refcount UAF (#143); BSD/Darwin signal enum values; Async\signal() clobbering worker threads (#109).

Testing & CI

  • New randomized chaos suite (fuzzy-tests/): EvilPeer + Toxiproxy transport fault injection (#127/#129), mutation-block scenarios, 100% public-API coverage (167/167). Runs under TRUE_ASYNC_SCHED=random:{1,7,42,1337} per-PR on top of the deterministic FIFO run.

Ecosystem

  • true-async/server — async HTTP/1.1 · HTTP/2 · HTTP/3 server built on this runtime (v0.7.2).
  • Coordinated TrueAsync 0.7.0 release also tags php-src, phpredis, xdebug, and frankenphp; Docker images are published from true-async/releases.

v0.6.7

14 Apr 14:12

Choose a tag to compare

TrueAsync PHP 0.6.7 (php 8.6)

Highlights

  • FrankenPHP on Windows — both Release and Debug builds of FrankenPHP are now produced and shipped alongside the regular PHP artifacts.
  • Installer: optional FrankenPHP install — the Windows PowerShell installer now asks (or reads INSTALL_FRANKENPHP=true) whether to also install FrankenPHP next to php.exe.

Async extension

Added

  • PDO Pool: getAttribute() support for pool attributes. $pdo->getAttribute(PDO::ATTR_POOL_ENABLED) now returns true/false depending on whether the connection pool is active. PDO::ATTR_POOL_MIN and PDO::ATTR_POOL_MAX return the configured pool size limits (or false when pooling is disabled). PDO::ATTR_POOL_HEALTHCHECK_INTERVAL is a construction-only attribute and raises an error if read at runtime.

Fixed

  • Heap-use-after-free in await_all() / await_*() with string keys. When any await_* function received an array with non-interned string keys (e.g. from json_decode() or str_repeat()), the returned results/errors arrays had incorrect refcount on those keys. Root cause: async_waiting_callback_dispose was called twice per callback (once from zend_async_callbacks_remove during del_callback, once from ZEND_ASYNC_EVENT_CALLBACK_RELEASE), but did not check ref_count — it unconditionally called zval_ptr_dtor on the key each time, decrementing the string refcount twice instead of once. When the calling function's local variables were freed (i_free_compiled_variables), the already-freed string was accessed again. Fixed by adding a ref_count guard to async_waiting_callback_dispose: when ref_count > 1, decrement and return without touching resources; cleanup happens only on the final dispose (ref_count == 1).

Windows build

Added

  • FrankenPHP support for Windows (Release and Debug). Both build types now produce a slim *-frankenphp.zip addon archive containing only FrankenPHP-specific files: frankenphp.exe, libwatcher-c.dll, brotli*.dll and pthreadVC3.dll. The PHP runtime DLL and extensions are not duplicated — extract this archive on top of the main PHP package.
  • Debug FrankenPHP linkage. FrankenPHP's cgo configuration was split into cgo_windows.go / cgo_windows_debug.go gated by the zend_debug Go build tag. When building against a debug devel pack, the workflow passes -tags zend_debug so clang sees -DZEND_DEBUG=1, matching the signatures of _emalloc/_efree/_estrdup in php8ts_debug.lib (which gain ZEND_FILE_LINE_DC arguments in debug mode).
  • Installer: optional FrankenPHP install. installer/install.ps1 now exposes an INSTALL_FRANKENPHP environment variable and an interactive "Install FrankenPHP?" prompt. When enabled, the installer downloads the matching slim frankenphp addon archive (respecting the Release/Debug choice), verifies its checksum against the same sha256sums.txt, and extracts it on top of the main install — frankenphp.exe lands next to php.exe.

Fixed

  • Clang/MSVC ABI mismatch in frankenphp_extension.c. Calls to emalloc(sizeof(zval)) were routed through zend_alloc.h's __builtin_constant_p specialization to _emalloc_16, which MSVC-built php8ts.lib does not export. Replaced with safe_emalloc(1, sizeof(zval), 0) to hit the exported _safe_emalloc entry point.
  • strtok_r unresolved on Windows. The Windows CRT has no strtok_r. Replaced all four call sites in frankenphp_extension.c with php_strtok_r (portable wrapper from main/php_reentrancy.h).
  • Debug packaging hardcoded release DLL names. The Package FrankenPHP step tried to copy php8ts.dll, but debug builds ship php8ts_debug.dll (see win32/build/confutils.js PHPLIB). Packaging now globs php*ts*.dll and php*ts*.lib so both release and debug names work.

Installation

Windows (PowerShell)

irm https://raw.githubusercontent.com/true-async/releases/master/installer/install.ps1 | iex

To also install FrankenPHP non-interactively:

$env:INSTALL_FRANKENPHP="true"; irm https://raw.githubusercontent.com/true-async/releases/master/installer/install.ps1 | iex

Docker

docker pull trueasync/php-true-async:0.6.7-php8.6
docker pull trueasync/php-true-async:latest

v0.6.6

03 Apr 10:44

Choose a tag to compare

What's Changed

  • libuv bumped to 1.52.1 — versions below 1.52.1 have known IO-URING issues on Linux; all UNIX/Linux builds and Docker images now ship with the fixed version.
  • OPcache production config in Docker images — both php-true-async (Debian) and FrankenPHP images now ship with a tuned opcache.ini out of the box: JIT tracing mode, validate_timestamps=0 (files don't change in containers), 256 MB bytecode cache, 128 MB JIT buffer.

Performance Benchmarks

We ran a realistic benchmark against Laravel + PostgreSQL (10 SQL queries per request, 1000 req/s constant load, 30s duration) comparing TrueAsync against Swoole NTS, Swoole ZTS, and FrankenPHP Octane.

Throughput (16 workers)

Server req/s vs TrueAsync Dropped requests
TrueAsync 993 req/s 1.1%
Swoole NTS 599 req/s TrueAsync +66% ~38%
Swoole ZTS 601 req/s TrueAsync +65% ~36%
FrankenPHP Octane 556 req/s TrueAsync +79% ~41%

TrueAsync handles the full target load with just 4 workers. Blocking servers need ~25 workers to reach the same throughput — a 6× worker efficiency advantage.

Latency at 4 workers

Server P50 P95
TrueAsync 28 ms 60 ms
Swoole NTS 5,440 ms 5,630 ms
Swoole ZTS 5,320 ms 5,520 ms
FrankenPHP Octane 5,240 ms 5,390 ms

The entire difference is queue wait: with 4 blocking workers at 1000 req/s, requests sit in queue for ~5,400 ms. TrueAsync coroutines handle requests immediately — queue wait is ~0 ms.

Memory footprint (idle)

Server 4 workers 16 workers
TrueAsync 147 MB 326 MB
Swoole NTS 481 MB (+227%, 3.3×) 762 MB (+134%, 2.3×)
Swoole ZTS 512 MB (+248%, 3.5×) 765 MB (+135%, 2.3×)
FrankenPHP Octane 357 MB (+143%, 2.4×) 421 MB (+29%, 1.3×)

Each Swoole worker bootstraps its own full copy of the Laravel application (~22 MB/worker). TrueAsync coroutines share the bootstrap within a single worker — ~2.5 MB per coroutine, ~89% less than Swoole (9× lighter).

Full results and charts: https://github.com/YanGusik/ta_benchmark/blob/main/RESULTS.md


Installation

Docker (fastest)

# Standard PHP CLI / FPM image
docker run --rm trueasync/php-true-async:latest php -r "var_dump(extension_loaded('true_async'));"

# FrankenPHP async server
docker run --rm -p 8080:8080 trueasync/php-true-async:latest-frankenphp

Linux (Ubuntu / Debian)

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | bash

macOS

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-macos.sh | bash

Windows

Download the pre-built ZIP from the releases page.


Verify installation

var_dump(extension_loaded('true_async')); // bool(true)
var_dump(ZEND_THREAD_SAFE);               // bool(true)

Full Changelog: v0.6.5...v0.6.6

v0.6.5

29 Mar 08:07

Choose a tag to compare

What's Changed

FrankenPHP Async Worker

v0.6.5 ships with full support for FrankenPHP in async worker mode — a single PHP thread now handles many concurrent requests, each running as a coroutine. While one coroutine is waiting for I/O (database query, HTTP call, file read), the scheduler runs other coroutines on the same thread.

Traditional FPM / standard FrankenPHP:
  1 request → 1 thread  (blocked during I/O)

TrueAsync FrankenPHP:
  N requests → 1 thread  (coroutines, non-blocking I/O)

Performance

  • Waker inline storage optimization: Embedded 2 trigger slots and 2 callback slots directly into the Waker struct, eliminating heap allocations for the most common case (1–2 events per await). Benchmarks: await 2.13 → 0.67 μs (~3×), await_all ×2 3.88 → 1.38 μs (~3×), Channel send/recv 1.48 → 0.50 μs (~3×).
  • Adaptive fiber pool sizing: The fiber context pool now grows dynamically based on coroutine queue pressure instead of a fixed pool size of 4. Yields 10–15% improvement in context switch throughput (10k coroutines × 10 suspends: 490 → 566 switches/ms).

Special thanks to YanGus for helping the project!

Async Laravel is in progress

https://github.com/YanGusik/laravel-spawn/
Right now, Laravel is being adapted for TrueAsync,
which, according to preliminary tests, can deliver a 3x or greater improvement in I/O performance!
And they said this technology wasn’t needed by anyone :)

Changed

  • ZEND_ASYNC_SUSPEND no longer throws an error when called with an empty array of events.

Fixed

  • SIGSEGV in pool healthcheck callback: Corrupted pool event structure fields caused a segfault when the pool was closed. Fixed by embedding a proper zend_async_event_callback_t inside async_pool_t.
  • proc_close() crash when child process already reaped: Handled ECHILD in async_wait_process() and libuv_process_event_start().
  • Pool acquire with failed factory caused use-after-free: Fixed by checking EG(exception) after factory failure and returning immediately.
  • Missing exception checks in pool error paths: Added EG(exception) checks in healthcheck loop and other error paths.
  • Pool close() now chains destructor exceptions via previous: All resources are destroyed and exceptions are chained via zend_exception_set_previous() instead of being silently discarded.
  • Pool destructor exceptions now propagate: Removed silent suppression via zend_clear_exception().

Installation

Docker (fastest)

# Standard PHP CLI / FPM image
docker run --rm trueasync/php-true-async:latest php -r "var_dump(extension_loaded('true_async'));"

# FrankenPHP async server
docker run --rm -p 8080:8080 trueasync/php-true-async:latest-frankenphp

Linux (Ubuntu / Debian)

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | bash

To include FrankenPHP:

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | \
  BUILD_FRANKENPHP=true NO_INTERACTIVE=true bash

macOS

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-macos.sh | bash

Windows

Download the pre-built ZIP from the releases page.


Verify installation

var_dump(extension_loaded('true_async')); // bool(true)
var_dump(ZEND_THREAD_SAFE);               // bool(true)

Full Changelog: v0.6.4...v0.6.5

v0.6.3

25 Mar 12:38

Choose a tag to compare

What's Changed

Fixed

  • Scope::awaitCompletion() deadlock: async_scope_notify_coroutine_finished() was not calling scope_check_completion_and_notify(), so awaitCompletion() always waited the full timeout even after all coroutines had finished. Also fixed awaitAfterCancellation to use ZEND_ASYNC_WAKER_DESTROY and correctly check zend_async_resume_when return value.

  • Scope dispose use-after-free: scope_dispose now keeps ref_count=1 as a guard during disposal and drops it only before efree. Removes a premature DEL_REF that caused use-after-free when finally handlers created child scopes. finally_handlers_iterator_dtor now uses ZEND_ASYNC_SCOPE_RELEASE to avoid double-decrement.

  • Poll event leak on negative stream timeout: In network_async.c, a negative tv_sec caused a poll event refcount leak. Fixed by guarding against negative timeout values.

Full Changelog: v0.6.2...v0.6.3

v0.6.2

24 Mar 14:00

Choose a tag to compare

What's Changed

Added

  • Non-blocking flock(): flock() no longer blocks the event loop. The lock operation is offloaded to the libuv thread pool via zend_async_task_t, allowing other coroutines to continue executing while waiting for a file lock.
  • zend_async_task_new() API: New factory function for creating thread pool tasks, registered through the reactor like timer and IO events. Replaces manual pecalloc + field initialization.

Fixed

  • await_*() deadlock with already-completed awaitables: When a coroutine or Future passed to await_all(), await_any_or_fail(), or other await_*() functions had already completed, it was skipped entirely (ZEND_ASYNC_EVENT_IS_CLOSEDcontinue), but resolved_count was never incremented. Since total still counted the skipped awaitable, resolved_count could never reach total, causing a deadlock. Fixed by using ZEND_ASYNC_EVENT_REPLAY to synchronously replay the stored result/exception through the normal callback path, correctly updating all counters. Additionally, when replay satisfies the waiting condition early (e.g. await_any_or_fail needs only one result), the loop now breaks immediately instead of subscribing to remaining awaitables and suspending unnecessarily.

Full Changelog: v0.6.1...v0.6.2

TrueAsync v0.6.1

15 Mar 08:24

Choose a tag to compare

Install

Windows

Build Download
Release php-trueasync-0.6.1-php8.6-windows-x64.zip
Debug php-trueasync-0.6.1-php8.6-windows-x64-debug.zip
Checksums sha256sums.txt

Quick install (PowerShell):

irm https://raw.githubusercontent.com/true-async/releases/master/installer/install.ps1 | iex

Docker (Linux)

docker pull trueasync/php-true-async:0.6.1-php8.6
docker run --rm trueasync/php-true-async:0.6.1-php8.6 php -v
Tag Description
0.6.1-php8.6 Ubuntu 24.04
0.6.1-php8.6-alpine Alpine
0.6.1-php8.6-debug Debug build

Build from Source (Linux)

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | bash

Build from Source (macOS)

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-macos.sh | bash

Documentation

Full documentation, API reference, and guides at true-async.github.io


What's Changed

Fixed

  • feof() on sockets unreliable on Windows: WSAPoll(timeout=0) fails to detect FIN packets on Windows, causing feof() to return false on closed sockets. Fixed by skipping poll for liveness checks (value==0) and going directly to recv(MSG_PEEK). On Windows, MSG_DONTWAIT is unavailable, so non-blocking mode is temporarily toggled via ioctlsocket. Errno is saved immediately after recv because ioctlsocket clears WSAGetLastError(). Shared logic extracted into php_socket_check_liveness() in network_async.c to eliminate duplication between xp_socket.c and xp_ssl.c.
  • Pipe close error on Windows: php_select() incorrectly skipped signaled pipe handles when num_read_pipes >= n_handles, causing pipe-close events to be missed and proc_open reads to hang. Fixed by removing the num_read_pipes < n_handles guard so PeekNamedPipe is always called for signaled handles.

Full Changelog: v0.6.0...v0.6.1

TrueAsync v0.6.0

13 Mar 21:04

Choose a tag to compare

Install

Windows

Build Download
Release php-trueasync-0.6.0-php8.6-windows-x64.zip
Debug php-trueasync-0.6.0-php8.6-windows-x64-debug.zip
Checksums sha256sums.txt

Quick install (PowerShell):

irm https://raw.githubusercontent.com/true-async/releases/master/installer/install.ps1 | iex

Docker (Linux)

docker pull trueasync/php-true-async:0.6.0-php8.6
docker run --rm trueasync/php-true-async:0.6.0-php8.6 php -v
Tag Description
0.6.0-php8.6 Ubuntu 24.04
0.6.0-php8.6-alpine Alpine
0.6.0-php8.6-debug Debug build

Build from Source (Linux)

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | bash

Build from Source (macOS)

curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-macos.sh | bash

Documentation

Full documentation, API reference, and guides at true-async.github.io


What's New

Added

  • Async\OperationCanceledException: New exception class extending AsyncCancellation, thrown when an awaited operation is interrupted by a cancellation token. The original exception from the token is always available via $previous. Affects all cancellable APIs: await(), await_*() family, Future::await(), Channel::send()/recv(), Scope::awaitCompletion()/awaitAfterCancellation(), and signal().
  • TaskGroup (Async\TaskGroup): Task pool with queue, concurrency control, and structured completion via all(), race(), any(), awaitCompletion(), cancel(), seal(), finally(), and foreach iteration.
  • TaskSet (Async\TaskSet): Mutable task collection with automatic cleanup semantics. Provides joinNext(), joinAny(), joinAll() methods, plus foreach iteration with per-entry cleanup.
  • Deadlock diagnostics (async.debug_deadlock INI option): When enabled (default: on), prints detailed diagnostic info on deadlock detection.
  • TCP/UDP Socket I/O: Efficient non-blocking TCP/UDP socket functions without poll overhead via libuv handles. Includes sendto/recvfrom for UDP, socket options API (broadcast, multicast, TCP nodelay/keepalive).
  • Async File and Pipe I/O: Non-blocking I/O for plain files and pipes. Supported: fread, fwrite, fseek, ftell, rewind, fgets, fgetc, fgetcsv, fputcsv, ftruncate, fflush, file_get_contents, file_put_contents, file(), copy, tmpfile, readfile, stream_get_contents, stream_copy_to_stream.
  • Pipe/Stream Read Timeout: stream_set_timeout() now works for pipe streams (proc_open pipes, TTY).
  • Future Support: Full Future/FutureState implementation with map(), catch(), finally() chains.
  • Channel: CSP-style message passing between coroutines with buffered/unbuffered modes, timeout support, and iterator interface.
  • Pool (Async\Pool): Resource pool with CircuitBreaker pattern. Configurable min/max size, acquire()/tryAcquire()/release(), blocking acquire with timeout, factory/destructor/healthcheck callbacks.
  • PDO Connection Pooling: Transparent connection pooling for PDO with per-coroutine dispatch.
  • PDO PgSQL: Non-blocking query execution for PostgreSQL PDO driver.
  • PostgreSQL: Concurrent pg_* query execution with separate connections per async context.
  • Async\iterate(): Iterates over an iterable with optional concurrency limit and cancelPending parameter.
  • Async\FileSystemWatcher: Persistent filesystem watcher with foreach iteration and Awaitable interface.
  • Async\signal(): One-shot signal handler returning a Future. Supports optional Cancellation.
  • Acting coroutine for error context: zend_async_globals_t.acting_coroutine — error reporting falls back to the coroutine suspended execute_data. Zero-cost.

Changed

  • Breaking: onFinally() renamed to finally() on Async\Coroutine and Async\Scope.
    • Migration: Replace ->onFinally(fn) with ->finally(fn).
  • Breaking: Async\CancellationError renamed to Async\AsyncCancellation, now extends \Cancellation instead of \Error. \Cancellation is a new PHP core root class per the True Async RFC.
    • Migration: Replace catch(Async\CancellationError $e) with catch(Async\AsyncCancellation $e) or catch(\Cancellation $e).
  • Bailout handling: During bailout (OOM), PHP-level handlers are skipped. Removed spurious graceful shutdown warning.
  • TaskGroup completion semantics: ASYNC_TASK_GROUP_F_COMPLETED set only when sealed and all tasks settled.

Fixed

  • Async file IO position tracking: Replaced bare lseek/_lseeki64 with zend_lseek. Fixed append-mode offset init and fseek behavior on Windows.
  • Reactor deadlock on pending file I/O: uv_fs_* requests were invisible to ZEND_ASYNC_ACTIVE_EVENT_COUNT, causing premature reactor exit.
  • Generator segfault in fiber-coroutine mode: NULL execute_data dereference in zend_generator_resume during shutdown. Fixed by checking ZEND_ASYNC_CURRENT_COROUTINE with ZEND_COROUTINE_IS_FIBER.
  • exec() output not split into lines in async path: On-the-fly line parser with zero-copy optimization, matching POPEN path behavior.
  • exec() exit code race condition: exec_on_exit is now the sole notification point.
  • Deadlock in proc_close() on Windows: Job Objects sent duplicate exit events, causing premature reactor shutdown.
  • Use-after-free in zend_exception_set_previous: Added identity checks before all calls.
  • Memory leak of Async\DeadlockError in scheduler fiber exit path.
  • stream_select() ignoring PHP-buffered data: Fixed hangs in run-tests.php -j on macOS.
  • Waker events not cleaned on resume outside scheduler.
  • False deadlock detection: Added ZEND_ASYNC_REACTOR_LOOP_ALIVE() check.
  • TaskSet auto-cleanup race condition: Deferred cleanup to point of result delivery.
  • Windows concurrent append (known limitation): Marked XFAIL (test 069).