Repository navigation
Report sync API runtime failures through return values - #406
Conversation
A failed size query, a busy release and a failed FIONREAD are ordinary runtime outcomes, so they now come back through the return value like every async operation, and a closed object reports bad_file_descriptor there too. A failed release leaves the object owning the handle and carries the invalid sentinel, never descriptor 0.
An option the kernel rejects is a runtime outcome, and so is a closed socket, so both now come back through the return value. Bench setup checks every set_option call instead of letting a failed TCP_NODELAY skew its numbers.
… with make_* factories Unparseable input is a runtime outcome, so the make_* factories are now the only way to build an address or endpoint from a string. make_local_endpoint replaces the local_endpoint path constructor, reporting filename_too_long for a path the socket address cannot hold instead of throwing it.
|
An automated preview of the documentation is available at https://406.corosio.prtest3.cppalliance.org/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-10-27 17:40:59 UTC |
|
GCOVR code coverage report https://406.corosio.prtest3.cppalliance.org/gcovr/index.html Build time: 2026-10-27 18:01:35 UTC |
Benchmark report
linux — full results (226 benchmarks, 5 iterations, 1.5s each)accept_churn
fan_out
http_server
local_socket_latency
local_socket_throughput
socket_latency
socket_throughput
windows — full results (77 benchmarks, 5 iterations, 1.5s each)accept_churn
fan_out
http_server
socket_latency
socket_throughput
macos — full results (113 benchmarks, 5 iterations, 1.5s each)accept_churn
fan_out
http_server
local_socket_latency
local_socket_throughput
socket_latency
socket_throughput
Run & raw JSON artifacts · flag rule: |median Δ| > max(2%, 3×CV of the noisier side) · advisory only |
tcp_server::bind built each port through the throwing tcp_acceptor convenience constructor and caught the exception to return its code. Both now share one exception-free helper that opens, configures address reuse, binds, and listens, so bind returns the failing step's code directly and the constructor throws it. On Windows both still claim the port exclusively, so a second listener fails with address_in_use on every platform.
A loopback pair that cannot be built is a runtime outcome like any other, so make_socket_pair and make_mocket_pair now return it, closing the sockets they carry back.
Out-of-range max_events_per_poll and inline_budget_max now fail io_context construction with std::invalid_argument, like thread_pool_size, instead of std::out_of_range from deep inside the reactor. The check runs whichever backend the context uses, so an io_uring context now rejects out-of-range values too. Where epoll is available, max_events_per_poll is also capped at INT_MAX / sizeof(epoll_event), the most one epoll wait accepts, so a larger value fails construction instead of every run().
On the 5.15 and 6.1 kernel lines a ring wait can return EBUSY when the completion queue is full and the kernel cannot flush overflowed completions into it. Reaping makes room, so the scheduler drains the queue and waits again rather than throwing out of run().
The rulebook and guide now say what the code does: runtime failures and closed objects report through the return value, and constructors throw only for root setup, contract violations, and the convenience constructors that compose code-returning calls. run(), poll() and their siblings document the run-loop throw, a demultiplexer the context cannot continue past.
connect_pair is noexcept, but on Windows it built its rendezvous with calls that throw: the throwing temp_directory_path, random_device, path and string allocation, and the std::thread that runs the connecting side. Any of those failing escaped the noexcept and terminated the process. A random_device failure now falls back to a seed from the clock, process and thread IDs. The other failures come back as error codes, with the listener and temporary directory released, and a refused worker thread returns before the accept loop could wait on a connection that will never arrive.
The native acceptors' value-returning accept() threw logic_error on a moved-from acceptor, because its awaitable built the peer socket up front from the acceptor's context. The peer is now built only on success, as the virtual-dispatch acceptors already do, so a moved-from acceptor completes with bad_file_descriptor like a closed one and the initiator never throws. The native awaitables also close a peer accepted for an awaiter destroyed before it resumes, which they previously left open. The native acceptor's closed and moved-from accept tests no longer depend on SO_REUSEPORT, so they now run on every platform.
The test and snippet sources silenced unused values with (void) casts. Following rulebook section 6, unused names are now declared [[maybe_unused]], and std::ignore is kept for values deliberately discarded, such as the inotify event a doc snippet reads outside its published region.
When the last handler's completion ran a Windows context out of work, work_finished() stopped it from inside noexcept, so a failed shutdown post called std::terminate. When the thread that ran out of work is running the context, the failure is now recorded, and that thread's run or poll call (run, run_one, run_for, run_until, poll, poll_one) retries the wakeup on its way out and throws std::system_error, the run loop's documented channel, only if the retry fails too. On any other thread, such as one releasing a work guard, nothing would retry the wakeup and threads blocked without a timeout would never wake, so that case still terminates. A failed post no longer leaves the context marking a wakeup as queued, so a later stop() retries it instead of returning silently while blocked threads stay asleep, and a failed post that leaves a zero last error is still reported as a failure. A direct stop() still throws, and also records the failure, so a run call on the stopped context retries the wakeup instead of blocking while work remains. restart() clears a recorded failure.
Fourteen headers included detail/except.hpp without calling any of its throw helpers. win_wait_reactor.hpp, which does throw and only reached the header through win_scheduler.hpp, now includes it directly, and the two IOCP acceptor services include <system_error> themselves instead of through it.
…onstructors A native type is a drop-in replacement for its base, but the native acceptors lacked the open, bind, and listen constructors and the native signal set lacked the executor forms, so switching a declaration to the native type stopped compiling. The acceptors gain protected base constructors that adopt the native service's handle before opening, so the result stays native and throws the same codes as the base constructor.
9f58b29 to
5e91ab4
Compare
Synchronous calls now report runtime failures the same way async operations do: through the return value. A closed object reports
bad_file_descriptorthere too. Exceptions are left for root setup, contract violations, a run loop that cannot continue, and the convenience constructors that compose code-returning calls.Breaking changes
size(),available(),release()throwcapy::results<T>set_option()/get_option()throwstd::error_code/capy::results<Option>ipv4_address("…"),ipv6_address("…"),ip_address("…"),endpoint("…"),local_endpoint("…")make_ipv4_address,make_ipv6_address,make_ip_address,make_endpoint,make_local_endpointmake_socket_pair/make_mocket_pair(test) throwcapy::results<…>max_events_per_poll/inline_budget_maxthrewstd::out_of_rangefrom the reactorio_contextconstruction throwsstd::invalid_argumenton every backendA failed
release()leaves the object owning the handle and returns the invalid sentinel, never descriptor 0.The throwing convenience constructors stay:
tcp_acceptor/local_stream_acceptor(context or executor, endpoint, backlog) andsignal_set(context or executor, signals...), along with their native counterparts.Fixes
EBUSYis retried instead of throwing out ofrun(). This happens on the 5.15 and 6.1 kernel lines when the CQ overflows.work_finished()no longer callsstd::terminate. The next run or poll call retries the wakeup and throwsstd::system_erroronly if the retry also fails.connect_pair: setup failures come back as error codes instead of escapingnoexcept.accept()on a moved-from acceptor: completes withbad_file_descriptorinstead of throwinglogic_error. A peer accepted for a destroyed awaiter is now closed.Other changes
tcp_server::bindbinds without exceptions.std::ignoreinstead of(void)casts.detail/except.hppincludes are removed.Each of the 14 commits builds and passes the test suite on its own.
Resolves #400