Skip to content

Stop promising a fatal counts wherever it is raised - #367

Merged
diolektor merged 1 commit into
mainfrom
fix/worker-mode-docs-overstate-where-a-fatal-counts
Sep 19, 2026
Merged

diolektor merged 1 commit into
mainfrom
fix/worker-mode-docs-overstate-where-a-fatal-counts

Conversation

@diolektor

Copy link
Copy Markdown
Contributor

Problem

The Recycling section of docs/features/worker-mode.md presented the consecutive-error breaker as counting a request that comes apart "wherever it is raised, a shutdown function and a destructor run at the end of the request included". That holds for three places and fails for two. The arm that reports an uncaught exception and the arm that closes the output buffers and sends the headers both recover the bailout and set no outcome flag, so a request that fatals there is finalized down the success branch — which does not merely fail to count it, it clears the run other requests had built. Both windows execute ordinary userland code: a __toString() reached while the exception is reported, an ob_start() callback, a header_register_callback() callback. The overstatement therefore ran in the direction that costs a reader something: they plan for a breaker that sees every fatal, and two paths wipe the count instead.

The paragraph under the outcomes table then justified counting by what a failure leaves behind — "a fatal there leaves the same wreckage the next request on that worker would inherit" — and in the next sentence made a max_execution_time deadline expiring in that same window neutral. A deadline is an ordinary fatal to the engine and is cleaned up by the same recovery, so it leaves the same state: the second sentence does not follow from the criterion the first one gives, and a reader who applies that criterion concludes the opposite of what the server does. The criterion the code actually applies is the cause — whether this worker is at fault — and only half of it had reached the documentation.

The behaviour of the two unflagged arms is a separate defect and is not changed here. The released 0.11.0 changelog entry carried the same universal and is narrowed with it.

What changed

  • The outcomes table row enumerates instead of generalizing: a fatal, an out-of-memory or a stack overflow raised in the request handler, in a shutdown function, or in a destructor that runs as those shutdown functions are released. The two unflagged paths are deliberately left undocumented rather than described: they are a tracked defect with a planned fix, and documenting them would have to be undone when it lands. The honest minimum is to promise only what holds.
  • The bullet above the table is restated in terms of cause. It had justified the recycle by the engine state a failure leaves and pointed the reader at the paragraph below for the classification — which, after this change, is the paragraph saying state is not what selects. Left alone it would have sent a reader back to the reasoning the change exists to remove.
  • The paragraph under the table gives the criterion in full: why a request ended, not what it left behind. Whatever raised the bailout, the worker picks the abandoned frames back up and rewinds the VM stack the same way, so a deadline in a shutdown function is cleaned up exactly as a fatal there is and leaves the worker in the same state — and is still neutral, because a request the server itself cut short is not evidence against the worker. The cancellation sentence names the two places it can land and keeps the condition that makes it true: a cancellation is neutral provided the request had not already hit a fatal. The sentence about order is scoped to the deadline case it can carry.
  • CHANGELOG.md, 0.11.0: "wherever it comes apart" removed, the enumeration after the colon kept.
  • llms-full.txt regenerated.

No behaviour changes — documentation only.

Verification

  • A grep for every shape the promise took — "wherever it is raised", "wherever it lands", "wherever it comes apart", "makes no difference to how it is read", "whatever happened afterwards", "which is what the recycle is for" — across docs/, llms.txt, llms-full.txt, README.md and CHANGELOG.md returns nothing. It returned four hits before the change.
  • scripts/gen-llms-txt.sh --check — up to date, 58 pages.
  • scripts/check-links.sh — 641 in-repo links, none broken.
  • No Rust or C source is touched on this branch, so the build, clippy and test jobs cannot be affected by it; CI runs them regardless.
  • Each place the new text describes was read in ext/oxphp_fiber.c: the three arms that raise the failure flag and are counted, the two that recover without one, the two shutdown-window arms that read PG(connection_status) for a deadline behind a "already came apart" guard, and the branch order in the scheduler's finalizer, which reads cancellation before failure.
  • The upstream facts the new text rests on were read at the pinned PHP version rather than from memory: sapi_send_headers() runs the userland header callback through sapi_run_header_callback(), which wraps it in no zend_try, so a fatal in it longjmps into the arm this text describes; php_output_end() pops the top handler through php_output_handler_op(), the userland output callback; and zend_timeout() ends in zend_error_noreturn(E_ERROR, …), taking the same error path as any other fatal, with only a bit on PG(connection_status) telling it apart. The interrupt helper checks the timeout flag before the interrupt function, so a deadline is recognized only by the two arms that read that bit — both of which carry the guard that keeps an established failure counted.

Docs:
  - Two of the five places a worker-mode fatal can land recover the bailout without flagging the request, so it is finalized as a success and *clears* the consecutive-error count. The recycling table, the bullet above it and the released 0.11.0 entry all promised the opposite; they now name the three places that do count.
  - The paragraph under the table justified counting by the engine state a fatal leaves, then called a deadline in the same window neutral although it leaves the same state. It now gives the criterion the code applies: why the request ended, not what it left behind.
@diolektor
diolektor merged commit 33eea23 into main Sep 19, 2026
7 checks passed
@diolektor
diolektor deleted the fix/worker-mode-docs-overstate-where-a-fatal-counts branch September 19, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant