Re: PHP True Async RFC Stage 4
| From: | Rob Landers | Date: | Tue, 21 Oct 2025 11:31:56 +0000 |
| Subject: | Re: PHP True Async RFC Stage 4 | ||
| References: | 1 2 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-128877@lists.php.net to get a copy of this message | ||
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Üõ¿‹x§Û3Of|?�
>> r
>>
>> 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