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

Reply via email to