Adoption triad - #340
Conversation
Registration errors now propagate as error codes instead of throwing through noexcept frames, a failed assign leaves the object unchanged with the caller owning the rejected fd, self-assign is rejected, and the family check is parameterized for the internet family. Stream release now clears the stale remote endpoint cache.
io_uring's local assign accepted any fd without checks and could not fail; iocp's closed the held socket before association could fail and left the endpoint caches empty. Both now validate first and seed the endpoint caches, sharing the POSIX validation helper.
release_socket() nulled the fd without cancel_and_flush, so pending SQEs could resolve against a recycled fd number after the caller closed the released descriptor. The acceptor already did this; the stream and datagram sockets now match.
Adoption validates family and type before touching the held socket, never mutates the adopted descriptor, and allows assign-over-open by cancelling pending operations first. release() cancels pending operations, deregisters, and hands the open descriptor to the caller. Implemented on every backend.
tcp_acceptor gains assign(), release(), and native_handle(); local_stream_acceptor gains assign() and native_handle(). Adopting a listening descriptor seeds the local-endpoint cache so accept paths that derive the address family from it keep working, including for adopted IPv6 listeners.
Socket wait(write) previously completed immediately and
unconditionally, which busy-spins external flush loops ('retry when
writable') exactly when the send buffer is full — intolerable once
foreign sockets can be adopted. Write waits now probe with a
zero-timeout poll and park until the reactor reports writability;
select snapshots include parked write waits; iocp routes write waits
through the poll reactor. Acceptor wait(write) keeps immediate
completion and is documented as meaningless.
A handle stays bound to its completion port for its lifetime under the documented API, so a released socket could never be adopted into an io_context again: re-association failed with ERROR_INVALID_PARAMETER. Sever the association in release_socket() with NtSetInformationFile(FileReplaceCompletionInformation), the same mechanism the wider ecosystem relies on and the reason release requires Windows 8.1 or later.
…se successes BSDs complete the handshake against a full accept queue and the listener then resets the child, so a success whose peer is already gone is a legal outcome there. The reset is recorded on the socket; the false-success bug under test reports success with nothing recorded, so consult SO_ERROR before counting the outcome as false.
Acceptor read and error waits now observe conditions that already hold when the wait begins: the reactor backends probe at initiation, and io_uring completes read waits from the multishot delivery queue — the accept machinery drains the kernel queue as connections arrive, so a poll on the listener could never report the readiness that matters. A connection queued before the wait, including one that predates adopting the listener, completes it everywhere. Acceptor write waits, which completed immediately on some backends and never on others, now fail uniformly with operation_not_supported: writability is meaningless for a listener, and a deterministic error beats a divergent lie.
The local stream and datagram sockets refused assign() on an open socket while every other adoptable type cancels, closes, and adopts — including the local stream acceptor. The front-end guards were the only difference; the backends already handle replacement. One family, one contract: pending operations complete canceled, validation failures leave the socket untouched, and the caller keeps a rejected descriptor.
A library that owns its socket needs readiness without ceding ownership; adopting a duplicate gives corosio a descriptor it may close while readiness still travels through the shared open file description. The old sketch predated assign() on tcp_socket; the example is now compiled.
A cancel reaching a wait after dispatch claimed it — or before it parked — latched a descriptor flag that outlived the op and canceled the next wait in the same direction under a fresh token. The latch was redundant: request_cancel() already marks the op, register_op consults that mark before parking, and delivery decodes it.
A second listen() re-registered the descriptor (failing on epoll) and on io_uring retired a live multishot arming without cancelling it, leaving two armings with the retired side closing its deliveries. Re-listen is now a backlog change only: registration and arming are skipped when already live. The listen path also purges deliveries a released descriptor left queued, as the adoption path always did.
The reactor backends reject with errno values every runtime maps to generic conditions; the Windows WSA equivalents map on some runtimes only. Normalize the adoption-contract codes the way the wait contract's already are, and pin the wrong-type rejection code in the tests.
|
An automated preview of the documentation is available at https://340.corosio.prtest3.cppalliance.org/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-08-19 16:49:35 UTC |
|
GCOVR code coverage report https://340.corosio.prtest3.cppalliance.org/gcovr/index.html Build time: 2026-08-19 16:59:52 UTC |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #340 +/- ##
===========================================
+ Coverage 79.78% 79.81% +0.02%
===========================================
Files 96 96
Lines 5926 5924 -2
Branches 1209 1209
===========================================
Hits 4728 4728
+ Misses 851 849 -2
Partials 347 347
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Resolves #337.