Zend async Scheduler ABI - #22561
Open
EdmondDantes wants to merge 56 commits into
Open
Zend async Scheduler ABI#22561EdmondDantes wants to merge 56 commits into
EdmondDantes wants to merge 56 commits into
Conversation
EdmondDantes
marked this pull request as draft
July 2, 2026 17:48
EdmondDantes
force-pushed
the
async-core
branch
4 times, most recently
from
July 2, 2026 18:11
615d050 to
ed9fe03
Compare
EdmondDantes
force-pushed
the
async-core
branch
10 times, most recently
from
July 3, 2026 09:58
f3b272d to
6e1a5ce
Compare
bwoebi
reviewed
Jul 3, 2026
bwoebi
reviewed
Jul 3, 2026
bwoebi
reviewed
Jul 3, 2026
bwoebi
reviewed
Jul 3, 2026
EdmondDantes
force-pushed
the
async-core
branch
5 times, most recently
from
July 16, 2026 08:21
3286ac0 to
9e95fa1
Compare
…heir upstream originals
The error stayed in EG(exception) across the switch and reached the coroutine entered: a destructor's await saw it and the coroutine it awaited finished unrun. The driving coroutine now finishes the pass itself when it resumes, as when no iterator could be created.
…uld not create The enqueue error stayed in the GC coroutine, which then ended with an exception no caller handles (a provider may end the request on it), and the run gave up its rerun. Clear it, as a missing iterator leaves none: the run is redone with the destructors called.
A fatal error, exit() or an uncaught exception in a destructor during shutdown bails out; shutdown_destructors() caught it, marked every object destructed and went on as if the pass had ended. The scheduler then ran the coroutines queued before it, with every object marked destructed, and in the iterator coroutine the pass went on to resume the main parked in another destructor. shutdown_destructors() now re-raises the bailout after its cleanup. In the iterator coroutine it ends the coroutine as a bailout, so the scheduler stops the queue as for a fatal error in a coroutine. On the main path zend_call_destructors() catches it and returns true, and php_request_shutdown() passes that to the scheduler's last hand-over. The catch also drops the pass owner, so the iterator ended by the bailout no longer warns at its release that it did not finish. 061 loses the deadlock and that warning: the main parked in ParksForever was left waiting only because the iterator swallowed the fatal error. 083 loses the second DeadlockError the final pass printed after the destructor's fatal error: that pass no longer runs, and the first fatal error already names the deadlock.
…scheduler honours
…me from new_coroutine ZEND_ASYNC_NEW_COROUTINE() now serves those callers and keeps the NULL check of the removed fallback: new_coroutine is optional at registration.
…the run-time caller of zend_async_scheduler_unregister
… bailout leaves set
…an their upstream originals
…ts_store *) Reverts a3834e2: the signature is core API an extension may call, so it keeps taking the store, and the iterator entry forwards to it again.
Reverts the alias removal of 0301846: ZEND_ASYNC_CLASS_NO, ZEND_ASYNC_IS_OFF, ZEND_ASYNC_IS_READY and the ZEND_ASYNC_CONTEXT_* and ZEND_ASYNC_INTERNAL_CONTEXT_* macros are core API an extension may use. The comment naming the run-time caller of zend_async_scheduler_unregister stays.
Reverts 9edcc5f: object_offset 0 marks a plain C coroutine with no PHP object again, ZEND_COROUTINE_OBJECT() returns NULL for it, and the fiber code checks for that before touching the object.
Reverts 653827c: a scheduler may again tell the engine's own GC and shutdown destructor coroutines apart from the others. ZEND_ASYNC_NEW_COROUTINE() keeps the NULL check that commit added, since new_coroutine is optional at registration.
Reverts 08e25de: the cancel slot takes is_safely again, and ZEND_ASYNC_CANCEL passes false.
Reverts 5e53ee2: new_coroutine takes extra_size again and ZEND_ASYNC_NEW_COROUTINE_EX() is back. Both macros keep the NULL check, since new_coroutine is optional at registration.
… zend_async_is_enabled() Reverts c440e48: the call_on_main_stack and coroutine_from_object slots, the OBJ_REF object model, ZEND_ASYNC_GET_EXCEPTION_CE, ZEND_ASYNC_CALL_ON_MAIN_STACK, zend_async_is_enabled(), zend_async_coroutine_from_object() and the globals destructor come back. The ext-scheduler-hook bridge calls coroutine_from_object, OBJ_REF and zend_async_is_enabled().
Reverts 28ef067: active_coroutine_count and ZEND_ASYNC_ACTIVE_COROUTINE_COUNT are back in the globals for a scheduler to keep.
Two incompatible changes on one day got the same date; a counter never repeats. Test 068 simulates a provider built for version 999.
The caller could not find the bytes: the coroutine sits at the head of the provider's larger struct, and the API has no offset to them. No caller passed a size and no provider honoured one; TrueAsync's new_coroutine has no such parameter. API version 2.
A provider registers once per process but wants async per request: it sets READY with ZEND_ASYNC_INITIALIZE in its RINIT, or when it registers at run time. Registration no longer sets READY. A request left OFF runs without a scheduler, so a run-time provider such as the ext-scheduler-hook bridge is not asked to launch before its PHP scheduler exists. TrueAsync gates its launch the same way.
…h ZEND_ASYNC_CANCEL_EX The comment said delivery is deferred; in TrueAsync a started coroutine cancelled safely is not interrupted at all: it becomes a zombie and runs to its end. ZEND_ASYNC_CANCEL_EX is the fork's macro that passes the flag.
…outine_count slot Nothing kept ZEND_ASYNC_ACTIVE_COROUTINE_COUNT up to date, so it always read 0. The scheduler knows its coroutines; the new slot asks it, and the macro gives 0 when no scheduler provides it. test_scheduler implements it and exposes it as TestScheduler\coroutineCount() for the test.
A coroutine without an object was allowed by the comments but had no use, and ZEND_COROUTINE_ADD_REF(), which new_coroutine tells callers to take, asserted on it. The fiber code now takes and drops its reference with ZEND_COROUTINE_ADD_REF()/ZEND_COROUTINE_RELEASE() instead of skipping a missing object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A lightweight asynchronous core without complex logic.
The idea is:
https://github.com/true-async/php-async-core-rfc/blob/main/scheduler_rfc.md
A detailed explanation of the core integration can be found here:
https://github.com/true-async/php-async-core-rfc/blob/main/core-integration.md
At the moment, both the code and the RFC are still under development. I'd be happy to hear your ideas and feedback.