Skip to content

Harden HTTP server against up-but-not-serving failure modes#11

Merged
johnathan-arsenault merged 1 commit into
mainfrom
jarsenault-http-serving-resilience
Jul 22, 2026
Merged

Harden HTTP server against up-but-not-serving failure modes#11
johnathan-arsenault merged 1 commit into
mainfrom
jarsenault-http-serving-resilience

Conversation

@johnathan-arsenault

Copy link
Copy Markdown

Problem

The agent process could stay alive while no longer accepting connections on its HTTP port. Investigation of the REST sink server lifecycle (Agent::startRestService::startServer::start/listen/acceptAsyncContext worker loop) turned up three independent causes.

Root causes & fixes

# Root cause Fix
1 Accept loop re-armed async_accept only on the success path, so any accept error (fd exhaustion/EMFILE, ECONNABORTED) permanently stopped the listener while the process stayed alive. Most likely cause of intermittent "up but not serving" outages. Recoverable errors now re-arm after a 100ms backoff (avoids hot-looping while a transient condition clears); operation_aborted (shutdown) still stops cleanly.
2 Bind failure used non-throwing asio overloads and returned silently — start()'s try/catch never fired, so a failed bind (EADDRINUSE, bad ServerIp, privileged port) left the process running but never listening. listen() now throws on open/bind/listen/set_option failure → start() logs fatal and std::exit(1) for supervisor restart.
3 io_context worker: a throwing completion handler unwound the only worker thread (default WorkerThreads=1), silencing all async serving while the main thread idled. Worker now catches exceptions and resumes run() (valid per asio: run() may be re-called after a handler throws).

Plus: Server::fail() now logs the failing-step description (previously discarded, making bind/accept failures undiagnosable), and a new getAcceptRearmCount() exposes accept-error recoveries for diagnostics.

Tests

Genuine regression tests added for each fix:

  • HttpServerTest.accept_loop_rearms_after_recoverable_error — asserts the re-arm counter goes 0→1 after an injected recoverable accept error. Verified it fails when Fix Jarsenault debify #1 is reverted (counter stays 0) and passes when restored.
  • HttpServerTest.listen_throws_when_port_is_already_bound — asserts listen() throws on a bind conflict.
  • AsyncContextTest.worker_survives_a_thrown_handler (new async_context_test.cpp) — asserts a second handler still runs after a prior handler throws.

Validation

Built and tested via the Ubuntu-22.04 CI-parity Docker image (gcc-13, -static-libgcc -static-libstdc++ — matches edge-device link requirements): 756/756 tests pass, exit 0.

Notes for reviewers

  • Behavior change in Fix pull in changes from parent fork #2: the agent now exits on an unbindable port instead of running headless. This is correct under systemd/a supervisor. If unsupervised operation where the agent should stay up on a bind failure is desired, this should be gated behind a config option — flagging for review.

🤖 Generated with Claude Code

The agent could stay alive while no longer accepting connections on its
HTTP port. Three independent causes are addressed:

- Accept loop: async_accept was only re-armed on the success path, so any
  accept error (fd exhaustion/EMFILE, ECONNABORTED) permanently stopped the
  listener. Recoverable errors now re-arm after a 100ms backoff;
  operation_aborted (shutdown) still stops cleanly.
- Bind failure: listen() used non-throwing asio overloads and returned
  silently on error, leaving the process running but never listening. It now
  throws so start()'s handler logs fatal and exits for supervisor restart.
- io_context worker: a throwing completion handler unwound the only worker
  thread, silencing all async serving. The worker now catches and resumes.

Also: Server::fail() now logs the failing-step description (previously
discarded), and getAcceptRearmCount() exposes accept-error recoveries for
diagnostics.

Adds regression tests for each fix (accept re-arm counter 0->1, listen()
throws on port conflict, io_context worker survives a thrown handler).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@johnathan-arsenault
johnathan-arsenault merged commit 7e00100 into main Jul 22, 2026
2 checks passed
@johnathan-arsenault
johnathan-arsenault deleted the jarsenault-http-serving-resilience branch July 22, 2026 15:32
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.

2 participants