router: keep a worker the deadline gave up on out of the idle economy - #412
Conversation
|
@claude review |
|
Claude finished @andypost's task in 7m 10s —— View job Review: PR #412 — keep a worker the deadline gave up on out of the idle economy
I read the whole chain rather than the diff alone: The core argument holds. The B2 exit really does not apply — a merely slow request emits no detached edge, so I checked the two things most likely to be wrong and both are correct:
One finding below is, I think, a real hole in the mechanism. 1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e199df1021
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
a8fe289 to
2db15e7
Compare
|
Addressed in 581c73f.
Re-verified with the count: |
|
@claude review |
|
Claude finished @andypost's task in 4m 27s —— View job Review: PR #412 — keep a worker the deadline gave up on out of the idle economy
|
| order | who takes the detached ref | who returns it |
|---|---|---|
| abandon, settle | nxt_router_app_abandon() (changed) |
settle (ours) |
| abandon ×2, settle ×2 | first abandon | second settle (detached_router 1→0) |
| abandon, START, FINISH, settle | abandon | settle — FINISH is a no-op (drop = 1) while detached_router != 0 |
| abandon, START, settle, FINISH | abandon | FINISH (changed, drop = 2) |
| START, abandon, settle, FINISH | START (drop = 0) |
FINISH (drop = 2) |
| START, abandon, FINISH, settle | START | settle (ours) |
| any, then port close | — | nxt_router_app_port_close(), which now zeroes all three (:6963-6966) |
The handover in both directions is the interesting case and both work: nxt_router_detached_apply()'s FINISH at :8033 refuses to clear while the router still has reasons, and the settle at :6782 refuses while the application does. Also correct that the settle only decrements under if (port->detached_router != 0) — after a port close zeroed it, a straggling settle drops its own two references and touches nothing else. And nxt_router_app_port_idle() still gates on active_requests == 0, so clearing the last reason cannot park a port that has ordinary live requests on it.
Field layout is uint8_t / uint8_t / uint16_t / uint32_t — nxt_port_t gains nothing in size.
Everything from my previous pass still holds: registration lifetime, the late reply, no second 503, and the nxt_router_response_error_handler() → settle → unlink order being safe on an already-unlinked rpc_data.
One new finding, of the same shape as the one just fixed.
1. detached_app is still a flag, so one FINISH edge ends another context's detached work — src/nxt_port.h:356, src/nxt_router.c:7994, :8025
libunit's detached state is per-context, not per-process: ctx_impl->detached (src/nxt_unit.c:448) is set by nxt_unit_request_done_detached() (:3644) and cleared by nxt_unit_ctx_detached_done() (:3680), and nxt_unit_send_detached() reports on each edge. With threads > 1 a process has several contexts, so two concurrent fastcgi_finish_request() responses in one worker emit START, START, FINISH, FINISH — all four mapping onto the same port, since nxt_router_detached_apply() is keyed by pid.
The router collapses them onto one bit:
- START₁:
detached_app = 1,detached0→1,changed = 1, keeps the ref; - START₂:
detached_appis already 1 anddetached != 0, sochanged = 0and no reason is recorded for context 2; - FINISH₁:
detached_app = 0,detached_router == 0,detached != 0→ clears,detached_processes--,nxt_router_app_port_idle()→ the port goes intoidle_portswhile context 2 is still running its detached work, and the reaper can QUIT it; - FINISH₂:
detached == 0,changed = 0, no-op.
References balance; the state does not. This is the exact failure mode you just fixed one field over, and it is inherited from fix/321-detached-worker-lifecycle rather than introduced here — but the commit message's reasoning ("a count rather than a flag because one worker can run several of them at once") applies verbatim to detached_app, and having the two reasons side by side with different arities is now the most confusing thing in the struct. Making both uint16_t counts, incremented on START and decremented on FINISH (with the same "clear only when both reach zero" rule), would make nxt_router_detached_apply() and nxt_router_app_abandoned_settle() structurally identical.
One wrinkle if you do: libunit retries a failed START/FINISH (nxt_unit_ctx_detached_retry(), src/nxt_unit.c:3716), and a duplicate START delivered on the wire would inflate a count where it is idempotent against a flag. The retry only fires when the previous send failed, so a duplicate should not be observable — worth confirming before switching, since the flag currently absorbs that class of mistake and a count would not.
2. Nothing in the suite can tell the count from the flag — test/test_php_detached_max.py
test_php_timed_out_request_keeps_the_worker_busy is a good test of the mechanism, but it drives a single request on a single-threaded worker, so it passes identically against e199df1's flag. The bug the count exists for needs two requests over limits.timeout in one process at once, which needs threads > 1 in the app config. Since src/test/nxt_router_app_timeout_test.c already exists and drives this area directly, a C-level case — two nxt_router_app_abandon() calls on one port, settle one, assert the port is still out of idle_ports, settle the other, assert it is back — would be cheaper and more deterministic than a threaded PHP test. Without either, the next refactor can quietly turn the count back into a flag.
3–5. Still open from the previous pass (all minor)
nxt_router_app_port_busy(task, app, port, "abandoned")is unreachable work —src/nxt_router.c:6714. A port with a live request cannot be inidle_ports/spare_ports, sostart_processis always 0 and thenxt_router_start_app_process()below it is dead. A one-line "defensive, symmetric with the START edge" comment stops a reader concluding a timeout can trigger a replacement spawn.app_port->main_app_portis the fieldnxt_router_detached_apply()deliberately avoids —:6700vs the comment at:7912. Not new (nxt_router_app_port_release()follows the same pointer at the same instant) and the app-level counter stays balanced either way, but the abandon now pins per-port state and a reference on that object for an unbounded time while the detached edge acts on whatevernxt_runtime_port_find()returns. Worth a sentence acknowledging that the two functions write the same field from two different port identities.- Test margin is about 2 s —
test/test_php_detached_max.py:373.hold = 8minus a 503 at ~2 s,time.sleep(IDLE_TIMEOUT * 2)(4 s) and fourapp_processes()round-trips. It would fail rather than pass spuriously, so it is a flake risk, not a false green; derivingholdfrom the measuredelapsedwould be steadier on a loaded runner.
6. Nit — declaration order, src/nxt_router.c:6741
nxt_app_t *app;
nxt_port_t *port;
nxt_bool_t adjust_idle_timer, ours;ascends in length; the file's convention (and nxt_router_app_abandon() right above it) is descending.
Not verified here
I reviewed by reading; I could not configure or build in this job (./configure is not in my allowed tools, and there is no pre-existing build/). The 13 passed / ./build/tests exit 0 results are yours as reported, not reproduced by me.
· branch fix/347-timeout-accounting
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 581c73f24f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Round 2 addressed in d0f14cd. 1. 2. The count has no test, and the body says so. The two deterministic routes both need something this PR does not have: PHP has no 3-5. The 6. Declaration order fixed. Re-verified on d0f14cd: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0f14cdb38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@claude review |
|
Claude finished @andypost's task in 5m 57s —— View job Review: PR #412 — keep a worker the deadline gave up on out of the idle economy
First, a branch-state problem that outweighs the code
nxt_assert(port->detached == 0);
if (nxt_slow_path(port->detached != 0)) {
- /*
- * A release build. A bare continue selects the same link again
- * and spins with app->mutex held. ...
- */
- nxt_queue_remove(lnk);
- lnk->next = NULL;
- app->idle_processes--;
continue;
}That block exists on the base tip and not here. This PR is the change that makes "a detached port in the idle queue" a state the router can reach for a new reason, so losing the release-build guard in the merge would be the worst possible regression to pick up. Please rebase onto the current Round 2 items: all addressed
The I re-traced the reference invariant — one detached app reference exists while One finding stands. 1.
|
d0f14cd to
67adeb3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 67adeb3e22
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
67adeb3 to
d1198aa
Compare
|
Round 3 addressed in the rebased, squashed head ( Branch state. Rebased onto the current base tip 1. Reference ordering. Fixed exactly as you and Codex describe, and it was a real hole: both application references ( 2. Minor. 3. The |
|
@claude review |
d1198aa to
39cabc7
Compare
|
Claude finished @andypost's task in 6m 27s —— View job Review: PR #412 — keep a worker the deadline gave up on out of the idle economy
Branch state is fixed
Round-3 items: all addressedThe reference ordering is fixed the way it needed to be. I re-traced the reference invariant — exactly one detached application reference exists while The drop order in Three things stand. 1. The abandon disarms the regression test for
|
|
Head moved -> : the squashed commit message lost its trailer block when it was rebuilt. Tree identical ( is empty), so the review above and the re-verification numbers still describe this head. |
39cabc7 to
f10bcb9
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Round 4 addressed in 1. The fixture comment and where 2. 3. The Re-verified on |
08fe69d to
031cc9b
Compare
f10bcb9 to
f7ea188
Compare
|
Rebased onto master now that #405 has merged ( On merged master this abandon sits directly after #405's new early return in Line references have shifted: the fall-through is now Validation after the rebase: debug and release builds with 0 warnings; Note on sequencing: PR #419 and the parked counter commit from #405's follow-up touch this same area. #419 is libunit-only and independent. The counter makes |
|
Claude finished @andypost's task in 6m 8s —— View job Review: PR #412 — keep a worker the deadline gave up on out of the idle economy
Branch state
The two conditions really are complements. Round-4 items are addressedThe fixture's I re-traced the reference invariant — exactly one detached application reference exists while One new finding. 1. The abandon also fires for an upgraded websocket, where its premise does not hold —
|
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
"limits": {"timeout"} answers the client 503 and then releases the request's
accounting. The worker is not told and keeps executing, so the release
returns a slot that is not free: the port is parked as idle, the reaper QUITs
a process in the middle of a request, and the next request can be counted
against a worker that is busy.
A request a worker is running is not the router's to release. The
acknowledgement is what moves the request's port off the shared queue and onto
the worker -- nxt_router_msg_retract() reads the same fact -- so when that has
happened the deadline enters the state a detached worker already has instead
of letting the release decide. port->detached is the term of the idle
transition that keeps the port out of idle_ports, and
nxt_router_app_port_busy() unwinds a port that is already there, both under
app->mutex, as the acknowledgement does. The unwind is defensive: a port with
a live request is not in an idle queue, so it normally finds nothing and
starts nothing.
detached_router counts every request the router gave up on rather than holding
one port-wide flag: two concurrent requests can exceed the deadline on one
worker, and with a flag the first answer cleared the port while the second was
still running. The application's own reason moved to detached_app, so a
FINISH edge cannot drop a request's reason and the two clears cannot drop each
other.
The state is settled by the worker's last message for the stream, which is
already where the answer of a request nobody tracks is dropped, and by the
port closing, which settles a detached worker today. Keeping the
registration alive until then is what makes the first edge reachable: the
unlink would cancel it, and the answer would be dropped by the port layer
with nothing left to settle the port.
The state is published to the main thread, which is where a port close settles
it, so both application references and the port reference are taken before the
critical section that publishes it -- the order nxt_router_detached_apply()
already uses. A close arriving in the window between the unlock and the
increments would otherwise drop a reference this call had not taken yet, and
the increment that followed would resurrect an application whose free was
already queued.
An application START edge arriving in the meantime records its own reason, and
its FINISH edge settles only that one, so a worker that answered and kept
running is never marked free by the response to the request the router gave up
on -- the case test_php_detached_start_after_the_worker_went_idle covers,
which now finds the port already held out rather than parked.
An upgraded stream is excluded. The 101 is a non-last message, so the
deadline is armed again and an idle websocket reaches this handler, but its
accounting was already returned by NXT_APR_UPGRADE: there is no slot being
held, and the mark would report the worker "detached" for the life of the
connection. The websocket state is the discriminator because it is assigned
beside that release and nowhere else, so the two cannot drift apart; a
handshake still in flight or one that failed does hold its accounting and is
still abandoned. The action the upgrade leaves behind is not used here: it is
a name a later change can rename without this condition noticing.
The state lands on app_port->main_app_port: the object the acknowledgement and
a release reach through the same field, while a detached edge finds it by pid,
and a worker's ports share one main port. /status reports such a worker under
"detached" while it runs, which is the router's own view of a worker it may
not hand out; that is the one semantic extension here and it is deliberate.
Nothing on the client's side changes: the 503 and its timing are what they
were, exactly one answer is sent, and the request still cannot execute twice.
Before the acknowledgement the router does not know which worker holds the
request, so that window is unchanged: it is the one-CAS race the C test
drives, where the request is either retracted or claimed by a worker that then
has to answer, and where the deadline still bounds the wait.
detached_app stays a flag, and libunit's detached state is per-context: with
"threads" > 1 a worker emits START, START, FINISH, FINISH for two concurrent
fastcgi_finish_request() calls, and the first FINISH ends the port's detached
state while the second context still runs. That is the state
#405 merged, not this change's, and a count would first have to show that libunit's retry of an undelivered edge cannot
deliver a duplicate START, which a flag absorbs and a count would not.
Test: test_node_websockets.py::test_node_websockets_timeout_does_not_detach
upgrades a stream under "limits": {"timeout"} and waits past the deadline with
no traffic in either direction, then asserts "detached": 0. Without the
exclusion it reads "detached": 1 and conftest reports leaked descriptors, the
port never being settled. Only the accounting is asserted: the handler also
answers 503 and drops the stream, which is wrong for a websocket and is issue
#422.
Test: test_php_detached_max.py::test_php_timed_out_request_keeps_the_worker_busy
holds the worker past the deadline and then answers normally, so no detached
edge reports anything. It asserts the router's own counters -- "detached": 1
with "idle": 0 past idle_timeout while the script still runs, the "ran" marker
appended exactly once, and the port settled when the answer arrives -- and it
fails on the pre-fix head with running: 0 and detached: 0, the worker having
been reaped mid-script. The observation window checks itself with a "done"
marker, so a loaded runner fails loudly instead of reading /status after the
script answered. The count itself has no test: a deterministic one needs
either a concurrent ASGI fixture or a C-level case driving two abandons on one
port, both of which need scaffolding this commit does not add.
Co-Authored-By: DeepSeek V4.1 Flash
Harness: DeepSeek Harness
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Confirmed, and confirmed on the mechanism too: application-to-client frames go out as Fixed in New test The 503 and close on that path are untouched here — that is issue #422, filed separately, since the deadline should not be firing on an established websocket at all. |
f7ea188 to
cb5f8a7
Compare
Second half of #347; the first half (arming the deadline for a request parked in
app->ack_waiting_req, and retracting it with a CAS before answering) is #405, which this is stacked on. No change to libunit, to the language modules or to the configuration.The branch is rebased onto the current base tip,
2db15e78:fix/321-detached-worker-lifecyclewas rewritten after this branch was cut (a8fe289fis no longer an ancestor), so the diff against the base is the seven files below rather than a merge of the two histories. The release-build recovery innxt_router_adjust_idle_timer()that the rebase brings in is present and untouched.B2 first: the finish edge does not cover this
The hard exit does not apply, and this is the chain that shows it.
A
limits.timeoutexpiry for a request a worker is running falls throughnxt_router_app_timeout()(src/nxt_router.c:7919-7921):nxt_request_rpc_data_unlink()(:1272) callsnxt_router_app_port_release()(:1282) withapr_action == NXT_APR_REQUEST_FAILED, set when the request was parked (:7327). That case isdec_requests = 1; inc_use = -1(:6785-6789), and the release then does:nxt_router_app_port_idle()is the idle transition, and its guard is (:6644-6649):The finish edge that would set the
detachedterm is emitted by libunit'snxt_unit_ctx_detached_done()(src/nxt_unit.c:3691), and it returns immediately unless a detached response was reported first:That flag is set only by
nxt_unit_request_done_detached()(:3612), called fromfastcgi_finish_request()(src/nxt_php_sapi.c:306). A request that is simply slow emits neither edge, so nothing holds the port out ofidle_ports: the deadline does release a worker that is still running. The exit does not apply and code is needed.What changes
The guard term is
port->detached == 0. When the acknowledgement has moved the request's port off the shared queue and onto a worker -- the factnxt_router_msg_retract()already reads at:1197-1200-- the deadline marks that worker's main port detached before releasing the request (nxt_router_app_abandon(),:6703). The release then still runs, butnxt_router_app_port_idle()finds thedetachedterm false and does not insert the port intospare_portsoridle_ports, so the reaper never sees it and the port keeps counting against"processes": {"max"}.nxt_router_app_port_busy()is called under the sameapp->mutexfor the port that is already in a queue, as the acknowledgement and the detached START edge do.The state is settled by
nxt_router_response_ready_handler()already drops (:5369-5385). The registration is kept alive until then (rpc_cancelcleared before the unlink) because the unlink would cancel it and the port layer would then drop the answer with nothing left to settle the port;nxt_router_app_port_close(),:6993-6999);nxt_router_detached_apply(),:8039and:8078). That is what keeps a worker which answered and kept running from being marked free by the response to the request the router gave up on: the existing casetest_php_detached_start_after_the_worker_went_idlenow finds the port already held out instead of parked.The late reply (B3)
With the registration kept, a response that arrives after the unlink reaches
nxt_router_response_ready_handler()withreq_rpc_data->request == NULL. The handler returns without touching the client (:5369-5385), closes no descriptor it did not open (the port dispatcher completes mmap buffers the handler leaves,src/nxt_port_socket.c:2049-2067), and touches no freed accounting: the request's pool and its app reference are already released, and the only state it reads is the registration's own. The last message settles the port once; a later message cannot settle it twice, because the pointers are cleared before anything else runs and the port bit is checked underapp->mutex.A second expiry cannot produce a second 503 or a second release: the fall-through unlinks, and
nxt_request_rpc_data_unlink()re-armsr->timerfor the pool release (nxt_router_http_request_release_post()), so the deadline handler is no longer armed for that request.limits.timeout(B4)The user-facing documentation (
unit-docs,source/configuration/index.rst) says:What this change alters is not the knob's meaning but its consequence: the cancellation still happens at the same moment and still answers 503, but it no longer returns the worker to the idle economy. The note's "it stays in the app's process pool" becomes true for a worker that is still executing the cancelled request; today it can be reaped instead.
The issue's scope note answers "does a post-acknowledgement timeout abandon the worker?" for this half -- only "the worker finished" should return capacity -- which is what this implements. What is not decided anywhere, and is left as an open question here, is what should eventually happen to that worker: this PR keeps it counted and waits for it to answer or die. If the intent is that the router should tell it to stop (a cancel message, a kill), that is a separate decision and this PR does not make it.
What does not change
src/test/nxt_router_app_timeout_test.cdrives, where the request is retracted or claimed by a worker that then has to answer; the deadline still bounds the wait./statusgains no field. A worker in this state is reported under the existingdetachedcount, which is the router's own view of a worker it may not hand out. That is the one semantic extension in this PR and it is deliberate: the count now means "held out of the idle economy", not only "the application said it was still running".Verification
Environment:
freeunit-harness/cialpine base pluspython3-dev openjdk21 nodejs npm openssl,./configure --tests --openssl --debugthen the php module, built in the same image as the run.a8fe289f, the branch point at the time)pytest -q test/test_php_detached_max.pypytest -q -k test_php_timed_out_request_keeps_the_worker_busythe worker running the failed request is not held out of the idle economy: {'running': 0, ..., 'idle': 0, 'detached': 0}(the worker had already been reaped)./build/tests(C unit tests, incl.router app timeout test)router app timeout test passedRe-run after the rebase onto
2db15e78(full rebuild,rm -rf build):test_php_detached_max.py13 passed,./build/testsexit 0, andtest_app_start_timeout.py+test_status.py12 passed / 6 skipped.The new case holds the worker 12 s against a 2 s
limits.timeoutand then answers normally, so no detached edge reports anything. It asserts, 4 s after the 503 and with the script still running (adonemarker says so), that/statusshowsdetached: 1andidle: 0, that the process is still there, that theranmarker was appended exactly once, and that the port settles when the answer arrives.Wire protocol
Nothing new, nothing renumbered.
NXT_PORT_MSG_*value is identical to2db15e78(and toa8fe289f: the base rebase does not touchsrc/nxt_port.h); the list insrc/nxt_port.his unchanged.nxt_port_handlers_tis byte-identical, in the same order, with the same size;NXT_PORT_MSG_MAX = sizeof(nxt_port_handlers_t)does not move.nxt_port_t.detached_router) and two pointers on the router's privatenxt_request_rpc_data_t.Review
The branch is one squashed commit. Four review rounds are in the comments; what they changed is in it:
detached_routercounts every abandoned request instead of holding one port-wide flag, and the application's reason moved to its own bit: two concurrent requests that exceedlimits.timeouton one worker each hold a reason, and the port leaves the detached state only when the last of them is answered (or the port closes) and no detached work of the application's own is running;port->detached, the ordernxt_router_detached_apply()already uses: a port close taking the same mutex could otherwise drop a reference this call had not taken yet, and the increment that followed would resurrect an application whose free was already queued;nxt_router_app_port_busy()call in the abandon path is commented as defensive (a port with a live request is not in an idle queue, so it normally unwinds nothing and starts nothing), and the state'smain_app_portidentity is recorded next to the pid-keyed one the detached edge uses;donemarker, so a loaded runner fails loudly instead of reading/statusafter the script answered;detached_routeris as wide asactive_requests(uint32_t):threadsis validated up toNXT_INT32_T_MAX, and a wrap would leave a settle unable to clear the state at all. The struct is the same size either way -- the four bytes of padding afteractive_requestsmove ahead of the counter;before=Ncomment and thetest_php_detached_start_after_the_worker_went_idledocstring describe whereidle_portscan still be reached now that the abandon holds the port out first, so thenxt_router_app_port_busy()unwind is not read as dead code.Known limitation, inherited (not fixed here)
detached_appis still a flag, and libunit's detached state is per-context: withthreads> 1 a worker emits START, START, FINISH, FINISH for two concurrentfastcgi_finish_request()calls, and the first FINISH clears the port while the second context is still running its detached work. That isfix/321-detached-worker-lifecycle's state, not this change's, and making it a count needs one thing established first: libunit retries an edge whose send failed, and a duplicate START that a flag absorbs would inflate a count and pin the worker out of the idle economy for good. Auditingnxt_unit_send_detached()/nxt_unit_ctx_detached_retry()for that is #405's path, so it is recorded here rather than changed.The imprecision now has a second clearing site:
nxt_router_app_abandoned_settle()readsdetached_appto decide whether the last router reason may clear the state, so a FINISH that ends one context's work can let a settle return the port to the idle economy while another context is still running. Same root cause, different caller; the eventual fix touches both.The count itself has no test either. A deterministic one needs either a Python ASGI fixture that holds two concurrent requests past
limits.timeout(PHP has nothreadsoption in this tree) or a C-level case driving two abandons on one port, which needsnxt_router_app_abandon()exported for tests the waynxt_router_test_app_timeout()is.test_php_timed_out_request_keeps_the_worker_busypins the mechanism end to end for one request; the count itself is verified by reading.Files