Re: PHP True Async RFC Stage 4

From: Date: Tue, 21 Oct 2025 06:46:45 +0000
Subject: Re: PHP True Async RFC Stage 4
References: 1  Groups: php.internals 
Request: Send a blank email to internals+get-128876@lists.php.net to get a copy of this message
On Sun, Oct 5, 2025, at 07:23, Edmond Dantes wrote: > Good day, everyone. I hope you're doing well. > > I’m happy to present the fourth version of the RFC. It wasn’t just me > who worked on it — members of the PHP community contributed as well. > Many thanks to everyone for your input! > > https://wiki.php.net/rfc/true_async > > **What has changed in this version?** > > The RFC has been significantly simplified: > > 1. Components (such as TaskGroup) that can be discussed in separate > RFCs have been removed from the current one. > 2. Coroutines can now be created anywhere — even inside shutdown_function. > 3. Added Memory Management and Garbage Collection section > > Although work on the previous API RFC was interrupted and we weren’t > able to include it in PHP 8.5, it still provided valuable feedback on > the Async API code. > > During this time, I managed to refactor and optimize the TrueAsync > code, which showed promising performance results in I/O scenarios. > > A test integration between **NGINX UNIT** and the **TrueAsync API** > was implemented to evaluate the possibility of using PHP as an > asynchronous backend for a web server: > ÷¹�Š-‡ > KI‘õ9ïhttps://github.com/EdmondDantes/nginx-unit/tree/true-async/src/true-async-php > > During this time, the project has come very close to beta status. > > Once again, I want to thank everyone who supported me during difficult > times, offered advice, and helped develop this project. > > Given the maturity of both the code and the RFC, this time I hope to > proceed with a vote. > > Wishing you all a great day, and thank you for your feedback! > Hey Edmond, I'm not quite finished notating the whole thing, but let's start with this: AWAITABLE There's something here that bothers me. The RFC says: > he Awaitable interface is a contract that allows objects to be > used in the await expression. > The Awaitable interface does not impose limitations on the > number of state changes. > In the general case, objects implementing the Awaitable > interface can act as triggers — that is, they can change their state an unlimited number of times. > This means that multiple calls to await <Awaitable> may > produce different results. But then coroutines say: > *Coroutines behave like Futures:* > once a coroutine completes (successfully, with an exception, or through cancellation), > it preserves its final state. > Multiple calls to await() on the same coroutine will always > return the same result or > throw the same exception. This seems a bit contradictory and confuses things. When I await(), do I need to do it in a loop, or just once? It might be a good idea to make a couple subtypes: Signal and Future. Coroutines become Future that only await once, while Signal is something that can be awaited many times. It probably won't change much from the C point of view, but it would change how we implement them in general libraries: if ($awaitable instanceof Trigger) { // throw or maybe loop over the trigger value? } CANCELLATIONS The RFC says: > In the context of coroutines, it is not recommended to use catch \Throwable > or catch CancellationError. But the example setChildScopeExceptionHandler does exactly this! Further, much framework/app code uses the $previous to wrap exceptions as they bubble up, so it might be nice to have an Async\isCancellation(Throwable): bool function that can efficiently walk the exception chain and tell us if any cancellation was involved. Minor nit: in the Async\protect section, it would be nice to say that cancellations being AFTER the protect() are guaranteed, and also specify reentry/nesting of protect(). Like what happens here: Async\protect(foo(...)); // foo also calls protect() And for reentrancy, foo() -> bar() -> foo() -> bar() and foo() calls protect(). Which one gets the cancellation? I also think that calling it a "critical section" is a misnomer, as that traditionally indicates a "lock" (only one thread can execute that section at a time) and not "this can't be cancelled". Also, if I'm reading this correctly, a coroutine can mark itself as canceled, yet run to completion; however anyone await()'ing it, will get a CancellationException instead of the completed value? DESTRUCTORS Allowing destructors to spawn feels extremely dangerous to me (but powerful). These typically -- but not always -- run between the return statement and the next line (typically best to visualize that as the "}" since it runs in the original scope IIRC). That could make it 'feel like' methods/functions are hanging or never returning if a library abuses this by suspending or awaiting something. ZOMBIES async.zombie_coroutine_timeout says 2 seconds in the text, but 5 seconds in the php.ini section. The RFC says: > Once the application is considered finished, zombie coroutines are given a time limit within > which they must complete execution. If this limit is exceeded, all zombie coroutines are canceled. What is defined as "application considered finished?" FrankenPHP workers, for instance, don’t "finish" — is there a way to reap zombies manually? Then there is dispose() and disposeSafely(), it would be good to specify ordering and finally/onFinally execution here. ie, in nested scopes, does it go from inner -> outer scopes, in the order they are created? When does finally/onFinally execute in that context? FIBERS Fibers are proliferant in existing code. It would be a good idea to provide a few helpers to allow code to migrate. Maybe something like Async\isEnabled() to know whether I should use fibers or not. Nit: the error message has a grammatical error: "Cannot create a fiber while **an** True Async is active" should be "Cannot create a fiber while True Async is active"? SHUTDOWN Is there also a timeout on Phase 1 shutdown? Otherwise, if it is only an exception, then this could hang forever. EXCEPTION IDENTITY The RFC says: > Multiple calls to await() on the same coroutine will always > return the same result or > throw the same exception. Is this "same exception" mean this literally, or is it a clone? If it is the same, what prevents another code path from mutating the original exception before it gets to me? TYPOS - fix file_get_content to file_get_contents in examples. - I think get_last_error() should be error_get_last()? - you call "suspend" a keyword in several places but it is actually a function. - examples use sleep(), but don't clarify whether sleep() will be blocking or non-blocking. - Sometimes you use AwaitCancelledException and other times CancellationError. Which is it? — Rob

« previous php.internals (#128876) next »