On Tue, Oct 21, 2025, at 08:46, Rob Landers wrote:
> 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:
>> 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

For bike shedding purposes:

It's also worth pointing out that "cancelled" is the British spelling, while 
"canceled" is the American spelling. PHP is already inconsistent in the 
docs/error messages, but I don't see any types/functions with either spelling. 
Generally, PHP tends to follow American spelling for the standard lib (color 
vs. colour, behavior vs behaviour, analyzes vs analyses, initialize vs 
initialise, serialize vs serialise, etc). So, IMHO, we should probably be using 
the american spelling here ...

— Rob

Reply via email to