Skip to content

net: end SocketServer's accept flow on a listener that can no longer accept - #881

Merged
Yaraslaut merged 3 commits into
masterfrom
fix/net-accept-dead-listener
Oct 8, 2026
Merged

Yaraslaut merged 3 commits into
masterfrom
fix/net-accept-dead-listener

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Fixes #872.

What was wrong

SocketServer::Core::acceptFlow returned only on Cancelled. Every other failed accept() waited 50 ms and tried again. Some listeners fail every attempt in the same way:

  • one that has stopped listening (EINVAL)
  • a closed or foreign descriptor (EBADF/ENOTSOCK)
  • a non-stream socket (EOPNOTSUPP)

For those, the flow woke every 50 ms forever, and port() kept reporting a port that nothing accepted on.

Reproduced at the current pin. #872 described this as only inferred, arriving with core-cpp ≥ 0.5.1's BadHandle. It already happens with core-cpp 0.5.0, the version pinned today. That version reports every failed accept as SystemError carrying the errno, and the loop retried that too.

The fix

  • morph::net::detail::acceptListenerIsDead() covers the failures that recur on every attempt. It accepts both core-cpp spellings: BadHandle/Unsupported, and SystemError with EINVAL/EBADF/ENOTSOCK/EOPNOTSUPP. The supported range (MORPH_CORE_CPP_VERSION 0.5) includes 0.5.0, so both are needed.
  • On a dead listener the flow ends and listenerLost() drops the listener. port() then reads 0, which is what the docs already promise when not listening, and a later listen() binds again. Connections already accepted are untouched.
  • Exhaustion (EMFILE, ENFILE, ENOBUFS, ENOMEM) is still retried. The existing "survives accept() running out of fds" test still passes.
  • docs/spec/core/backend.md now documents which accept failures are retried and which end the flow.

Verification

Measured locally with clang 20.1.2, Debug, the full strict warning set, and core-cpp 0.5.0 (the pin):

  • Integration test: "a listener that stops listening ends the accept flow instead of retrying it". It calls shutdown() on the server's listening descriptor underneath it, then checks three things:
    • port() reaches 0
    • a client connected beforehand still gets a reply
    • listen() works again
  • Table test for acceptListenerIsDead. It checks each condition three ways:
    • dead: both spellings
    • retried: exhaustion
    • retried: per-peer codes, even when they carry an errno that would mean dead under SystemError
  • Mutation 1, the fix reverted (the header from master): the integration test fails, because port() never reads 0 within 5 s.
  • Mutation 2, only the BadHandle/Unsupported check with no errno fallback: the integration test also fails. So at the current pin, the fallback is the part that does the work.
  • Branch coverage, from clang 20 coverage over morph_net_tests: every arm of the classifier, and of the flow's new branch, is taken both ways. The first push missed include/morph/net's 88% floor on untested arms; that is what the table test and the second commit fix.
  • The whole morph_net_tests suite passes: All tests passed (1223 assertions in 216 test cases).
  • CI is green on the head, including clang-coverage, clang-tidy-diff, clang-format, the sanitizers and Windows.

Not verified: macOS/BSD. The integration test relies on Linux's EINVAL from accept() on a shut-down listener; morph::net is POSIX-only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WeDnphcJ7EZBLwVfE6pqZu

claude added 3 commits October 7, 2026 12:27
…accept

The accept flow returned only on Cancelled; every other failed accept
waited 50 ms and tried again. A listener that has stopped listening
(EINVAL), a closed or foreign descriptor (EBADF/ENOTSOCK) or a non-stream
socket (EOPNOTSUPP) fails every attempt the same way, so the flow woke
every 50 ms forever and port() kept reporting a port nothing accepted on.

Classify those as a dead listener and end the flow: drop the listener so
port() reads 0 and a later listen() binds afresh, leaving connections
already accepted untouched. Recognise both core-cpp spellings: BadHandle/
Unsupported, and SystemError carrying the errno, which is what the pinned
core-cpp 0.5.0 reports for every failed accept. Exhaustion (EMFILE, ENFILE,
ENOBUFS, ENOMEM) is still retried.

The new test shuts the listening socket down underneath a running server.
Without the fix it fails (port() never reads 0 within 5 s); with only the
BadHandle check and no errno fallback it also fails at the current pin.

Fixes #872

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeDnphcJ7EZBLwVfE6pqZu
The clang-coverage gate dropped include/morph/net's branch coverage below
its floor: the classifier's BadHandle, Unsupported, EBADF, ENOTSOCK and
EOPNOTSUPP arms were never taken, because the integration test can only
produce EINVAL.

Make the classifier a free function, morph::net::detail::
acceptListenerIsDead, and table-test it: both spellings of every dead
listener, exhaustion (retried), and per-peer codes (retried even with an
errno that would mean dead under SystemError). listenerLost() loses its
null check, whose false arm could not be taken: only the accept flow calls
it, while its generation is current, and close() moves the generation
before it resets the listener.

Measured with clang 20 coverage over morph_net_tests: every arm of the
classifier and of the flow's new branch taken both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeDnphcJ7EZBLwVfE6pqZu
Two assertions ran past the column limit, and clang-format wraps such a
line differently across versions. A local lambda keeps every line short.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeDnphcJ7EZBLwVfE6pqZu
@codecov

codecov Bot commented Oct 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/net/socket_server.hpp 93.75% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit 9c0c7c6 into master Oct 8, 2026
39 checks passed
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.

net: SocketServer's accept flow retries a dead listener forever

2 participants