Repository navigation
Conversation
std::thread::spawn panics when the OS refuses a new thread, and the panic unwound the accept loop that raised it, leaving the process running with no listener behind its socket. Spawn through thread::Builder at both accept sites, log the failure, drop that one connection and keep accepting. Claude-Session: https://claude.ai/code/session_018yr6GvM4fkHH4HVEzwLhHM
hojinyoo
force-pushed
the
hojinyoo/api-accept-resilience
branch
from
September 9, 2026 13:35
9cd0ae1 to
3c641a5
Compare
Owner
Author
|
Superseded upstream: v0.9.4 herdrdev#4958 guards thread spawns in both the API accept loop and src/server/client_accept.rs. |
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.
Incident
The server stayed up but stopped answering
herdrCLI/API calls. The API acceptloop spawns a handler thread per connection with
std::thread::spawn, whichpanics when the OS refuses a new thread (EAGAIN under thread/fd pressure). That
panic unwound the listener thread and dropped the listener, so the process kept
running with nothing behind the API socket, and only a restart brought it back.
Changed sites
Both accept loops now spawn through
std::thread::Builder, log the failure andkeep accepting, so a refused thread costs one connection instead of the loop:
src/api/server.rs— the per-connection handler in the API accept loop.This is the site from the incident.
src/server/client_accept.rs— the per-client handshake in the thin-clientaccept loop. Same failure mode, worse blast radius: that loop runs on the
headless server's main event loop, so the panic would take the whole server
down along with every pane it owns.
Each site has a regression test that forces the spawn to fail through a
#[cfg(test)]hook and asserts the loop still accepts and serves afterwards.Cherry-pick
Commit for
build/v0.9.0:3c641a5f30045cddd6f0875cf508645dc271b7dcBranched from
b99002ac(release: v0.9.0), so it applies cleanly.Independent review
Codex was unavailable (usage limit), so the review pass ran on Grok. It found
that both regression tests would have stayed green if the production fix were
reverted, and it was right on both counts:
INITIAL_REQUEST_TIMEOUT, so the spawn hook was never observed. The droppedconnection now sends a real request and the test asserts it never reaches the
app channel.
next_client_idincrements before the spawn, so== 3held either way. Thehandshake test now probes
local_stream_peer_closed, proving the acceptedstream was dropped rather than handed to a handshake thread.
Both fixed, and verified by mutation: reverting either call site to
std::thread::spawnnow fails both tests in ~0.02s.Not addressed, by design: no test exercises
Builder::spawn's realErrpath.Forcing a genuine EAGAIN needs a process-wide
setrlimitthat would break theparallel test process, so the hook stands in for it.
Validation
Run on the repo-pinned 1.96.1 toolchain (the Homebrew 1.98 on this machine
shadows it and reports unrelated new lints):
cargo fmt --check— cleancargo clippy --all-targets --locked -- -D warnings— cleancargo clippy --bin herdr --locked --target x86_64-pc-windows-msvc -- -D warnings— cleancargo nextest run --locked --no-fail-fast— 3105 tests: 3103 passed, 2 failed, 2 skippedjust maintenance-test(101),just ui-hot-path-architecture-test(6),just integration-assets-test(5),just plugin-marketplace-test(31),just docs-contract-test(7) — all passThe two failures are
live_handoff_keeps_unmanaged_agent_name_bound_to_saved_sessionand
live_handoff_preserves_pane_process_io. Both fail identically on a detachedb99002acwithout this commit, so they are pre-existing local failures, not aregression from this change.
https://claude.ai/code/session_018yr6GvM4fkHH4HVEzwLhHM