Skip to content

Repair a stale busy entry instead of spinning on it - #173

Merged
mgrossmann merged 1 commit into
mainfrom
issue-159-stale-busy-recovery
Aug 11, 2026
Merged

Repair a stale busy entry instead of spinning on it#173
mgrossmann merged 1 commit into
mainfrom
issue-159-stale-busy-recovery

Conversation

@mgrossmann

Copy link
Copy Markdown
Contributor

Closes the remaining httpd half of #159: a stale httpd->busy entry is now
repaired on first contact instead of spinning a worker into the #160
escape.

What changes

serve_client() holds the one fact httppc() does not: ownership. In
worker mode a client comes off the work queue and belongs to exactly one
worker. Nothing else processes it -- clients never enter httpd->httpc in
worker mode, so http_process_clients() walks an empty array, and no module in
the fleet calls the busy API through the vector (checked across mvsmf,
httprexx, httplua).

So when a pass makes no progress and the client turns out to be in
httpd->busy, that entry is stale by definition -- left behind by an
earlier failure on this worker, or by a freed client that occupied the same
storage. The loop now clears it on the FIRST no-progress pass, reports
HTTPD907W, and keeps serving. The request completes normally; the wedge
never forms; the connection survives.

If the stalled client is NOT in busy, the stall has a different cause
(http_set_busy() failing on a full region is the known one) and nothing is
cleared -- the #160 spin guard remains as the last resort for that, unchanged.

The recovery sits in serve_client() and not in httppc() deliberately:
the busy-exit is legitimate concurrency control wherever ownership is not
certain (select mode, future vector callers). Only the owner may conclude
"stale", and serve_client() is the owner.

What this does and does not claim

The wedge has not been reproduced since the original specimens, so this is
defense at the choke point, verified by review and regression, not by
reproducing the wedge. What the change guarantees: no stale busy entry can
cost more than one log line
, where before it cost either a worker (pre-#160)
or an innocent connection plus 1000 ENQ/DEQ round trips (#160's escape).

The two mechanisms behind the original specimens are accounted for elsewhere:

  • The ENQ/DEQ spinners were observed before libc370#89's ___try() fix, on
    servers mid-CGI-abend-cascade -- i.e. with every CRT-anchored call on the
    worker resolving through a dead module's runtime. That environment cannot
    recur as-then.
  • The frozen-in-wait shape (client at CSTATE_IN, 10 s timeout never firing)
    now has a concrete code-level candidate: ecb_timed_waitlist() stores a
    failed STIMER's rc and waits anyway, on an ECB only that timer would post.
    Storage-gated, which matches "only on degraded servers, never fresh".
    Filed as ecb_timed_waitlist() ignores a failed STIMER and waits on an ECB nothing will post libc370#94.

Tests

MVS suite: 264 PASS, 0 FAIL (JOB00870), host suite 63 PASS, 0 FAIL. The
recovery itself is not black-box reachable (a stale busy entry cannot be
fabricated from outside), which is exactly the property the fix restores.

Refs #159

The spin guard from #160 made the wedge survivable: after 1000 no-progress
passes the worker forces the connection closed and returns to the pool.
That is a last resort wearing the wrong costume -- the client it closes is
innocent, and 1000 ENQ/DEQ round trips hammer the very lock every healthy
request queues behind.

serve_client() can do better than survive, because it holds the one fact
httppc() does not have: ownership.  In worker mode a client comes off the
work queue and belongs to exactly one worker -- this loop.  Nothing else
processes it: clients never enter httpd->httpc in worker mode, so
http_process_clients() walks an empty array, and no module calls the busy
API through the vector (checked across mvsmf, httprexx, httplua).  So when
a pass makes no progress and the client turns out to be in httpd->busy,
that entry is stale by definition -- left behind by an earlier failure on
this worker, or by a freed client that occupied the same storage.  Clear
it on the FIRST no-progress pass, say so (HTTPD907W), and keep serving:
the request completes normally and the wedge never forms.

If the stalled client is NOT in busy, the stall has a different cause --
http_set_busy() failing on a full region is the known one -- and clearing
nothing is correct: the spin guard remains as the last resort for that.

The recovery sits in serve_client() and not in httppc() deliberately.
httppc()'s busy-exit is legitimate concurrency control wherever ownership
is not certain: select mode (http_process_clients walking the client
array) and any future vector caller.  Only the owner may conclude
"stale"; serve_client() is the owner.

Refs #159
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