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