Stop promising the sleep hooks always suspend the fiber - #368
Merged
diolektor merged 1 commit intoSep 19, 2026
Merged
Conversation
Docs: - Where a fiber switch is refused, the hooked native sleep()/usleep() hand the call to the builtin they replaced and wait on the worker thread. Four public artifacts promised suspension unconditionally, and the reference page went further, asserting that a hooked sleep() always returns 0 — on that path it returns whatever the builtin returns, which for a signal-interrupted sleep() is the seconds it did not sleep. That sentence shipped in 0.11.0. - The places where switching is blocked are not a closable list: two of the six were named, and the rest run destructors after a request comes apart, where a __destruct() that sleeps holds the thread. The pages now state the rule — switching is blocked wherever leaving the current frame would be unsafe — with the places as examples, and say where the reader is safe: the ordinary end of a request runs destructors and shutdown functions with switching allowed. - A fiber a userland scheduler started is a separate case rather than another entry in that list: switching is not blocked there, OxPHP simply cannot resume a context it does not own. It is also about a fiber started inside a request rather than the shape of a whole deployment, since a worker-mode request cannot drive an event loop of its own. - oxphp_sleep() and oxphp_usleep() share that same fallback, so reaching for them is not a way around it — which is what these pages used to recommend.
diolektor
deleted the
fix/fiber-multiplexing-docs-promise-sleep-always-suspends
branch
September 19, 2026 21:23
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.
Problem
docs/features/fiber-multiplexing.mdtold the reader thatRUNTIME_HOOKS=sleepmakes nativesleep()/usleep()suspend the fiber, with no condition attached. The hooked pair suspends only where a fiber switch is possible; where it is not,oxphp_fiber_sleep_us()returns 0 and the hook hands the call to the builtin it replaced, which waits on the worker thread — the exact behaviour the note offered to remove. That page is what a reader sizesPHP_WORKERSfrom.The same unconditional promise stood in three more public artifacts, and one of them was not an omission but an outright false statement in released text.
docs/operations/configuration.mdasserted that "a hookedsleep()always returns0(the signal-interruption return value of the native builtin does not arise)". On the fallback path the call is the native builtin —PHP_FUNCTION(sleep)→php_sleep()→ libcsleep(3)— so a signal-interrupted sleep gives back the seconds it did not sleep. That sentence shipped in 0.11.0.Two further things the text got wrong, both in the direction that costs the reader something:
zend_fiber_switch_block()is called in six places; two were named. The other four run underzend_catchand execute userland destructors while switching is blocked, so a__destruct()that sleeps to back off holds the thread. The comment beside one of them says as much outright: "a destructor that sleeps or reads a socket does it without leaving this frame."Fiber::suspend()works normally inside an AMPHP or Revolt fiber. What refuses is OxPHP's own ownership check — it cannot resume a context it does not own. Reading it as "no fiber can be suspended at all" told a Revolt user their scheduler cannot work inside a request.The remedy the pages recommended did not work either:
oxphp_sleep()/oxphp_usleep()take the identical fallback, so pointing at them is not a way around the limit. And the troubleshooting entry titled with this exact symptom — "oxphp_sleep()not yielding to other requests" — named one cause and prescribed enabling a flag the reader has already enabled by the time they are reading it.What changed
streamshook section of the same page already said.configuration.md: the falsealways returns 0absolute is replaced by what the fallback path actually does; thesleepcategory row gains its condition; the bullet at the decision point no longer says the hooks suspend "unconditionally"; and the argument check is named as a real divergence — the hook rejects anything above4294967295, which the unhooked builtin accepts and truncates.docs/php/functions.mdandoxphp.stub.php: the fallback condition was given as "outside a fiber" only. It now also names a refused switch and a context OxPHP does not own.oxphp_async_await()carried the same guard and the same unqualified table row; it is qualified with the rest.CHANGELOG.md, 0.11.0: the list of contexts gains theFILTER_CALLBACKcase that shipped in that same release, is marked as non-exhaustive, and stops filing the userland scheduler under "suspending is refused".llms-full.txtregenerated.No behaviour changes — documentation only.
Verification
scripts/gen-llms-txt.sh --check— up to date, 58 pages.scripts/check-links.sh— 648 in-repo links, none broken.cargo fmt --check,cargo clippy --no-default-features -D warnings,cargo test --no-default-features— 1170 passed, 0 failed. No Rust or C source is touched on this branch, so these cannot be affected by it; CI runs them regardless.0", "unconditionally", "no fiber can be suspended", "the engine will not let switch" — acrossdocs/,README.md,CHANGELOG.md,llms.txt,llms-full.txtandoxphp.stub.phpreturns nothing that is still unqualified. The twoREADME.mdfeature bullets are left as they are: they summarize and link to the page that now carries the condition.ext/oxphp_sapi.candext/oxphp_fiber.c: the three guards inoxphp_fiber_sleep_us(), therc == 0delegation in both hooked functions, the identical bare-usleep()fallback inoxphp_sleep()/oxphp_usleep(), the same guard inoxphp_fiber_suspend_for_await(), and all sixzend_fiber_switch_block()call sites — including which of them sit in azend_catcharm, which is what makes the teardown cases post-bailout only and the ordinary request end safe.php_sleepissleep(3)on every platform this server runs on;PHP_FUNCTION(sleep)rejects only negative arguments and then returns what libc returns;PHP_FUNCTION(usleep)sets no return value, so no text here claims one for it;Fiber::suspend()refuses only for no active fiber, a destroyed fiber, or blocked switching, none of which holds inside a userland scheduler's fiber; and the ticks handler and pcntl's signal dispatch raise the block themselves, which is why only those two are attributed to upstream and the rest are not.