Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e28b4fe
test: syscall fault injection harness for every backend
sgerbino Aug 26, 2026
c70d523
ci: build the tests in the FreeBSD CMake leg
sgerbino Aug 26, 2026
ca8d5c4
fix(select): re-acquire the lock after an interrupted select
sgerbino Aug 26, 2026
aa92e57
fix(io_uring): report EAGAIN when the submission queue is full
sgerbino Aug 26, 2026
760cdf2
fix(io_uring): report a signal reader that could not be armed
sgerbino Aug 26, 2026
89a3032
fix(signal_set): report the real error when the self-pipe cannot be c…
sgerbino Aug 26, 2026
edae8e1
docs: correct the MSG_NOSIGNAL notes and state the run_task lock cont…
sgerbino Aug 26, 2026
8a8eb0e
fix(iocp): release the completion port when a nested service throws
sgerbino Aug 26, 2026
f249f49
fix(local_connect_pair): give up the accept when the worker cannot co…
sgerbino Aug 26, 2026
2fc8df0
fix(iocp): create the wait reactor's wakeup pair when the io_context …
sgerbino Aug 26, 2026
f208ba5
fix(io_uring): create the ring when the io_context is constructed
sgerbino Aug 26, 2026
9a33cfe
fix: make a failed self-wake best-effort on every backend
sgerbino Aug 26, 2026
c10c271
fix(iocp): stop accepting waits after the poll thread dies
sgerbino Aug 26, 2026
089cdff
fix: report available() and host_name() errors through make_err
sgerbino Aug 26, 2026
503fd70
fix(io_uring): keep work accounting symmetric for the multishot accep…
sgerbino Aug 26, 2026
35b877a
test(harness): count(), CQE flag clearing, hook_is_live, high-fd seam…
sgerbino Aug 27, 2026
c15e59b
test(harness): flush coverage counters before a forked child exits
sgerbino Aug 27, 2026
22fad08
test(select): descriptor-range rejections and the SIGPIPE guard
sgerbino Aug 27, 2026
775b880
test(posix): per-process signal suites, connect_pair rollback, local …
sgerbino Aug 27, 2026
b1c43ed
test(epoll): adopted acceptors and the accept the reactor dispatches
sgerbino Aug 27, 2026
f1552f7
test(kqueue): the acceptor paths this backend registers on its own
sgerbino Aug 27, 2026
9af2de1
test(io_uring): the object surface underneath the ring
sgerbino Aug 27, 2026
c45e58c
test(reactor): an error event for every kind of parked operation
sgerbino Aug 27, 2026
09047d7
test(iocp): scheduler fallbacks, the wait reactor's answers, the poin…
sgerbino Aug 27, 2026
0df2a99
test(win): the file, resolver and pair paths that answer without failing
sgerbino Aug 27, 2026
958383e
test(teardown): drain every queued completion and live implementation…
sgerbino Aug 27, 2026
4157a48
fix(iocp): ask the wait reactor for a bit this provider implements
sgerbino Aug 28, 2026
448d204
fix(pool): join the blocking-I/O workers before the scheduler drains
sgerbino Aug 28, 2026
7863401
fix(signal_set): give the registration back when the service shuts down
sgerbino Aug 28, 2026
6ece553
refactor(posix): move file ops onto the coro_op base
sgerbino Sep 1, 2026
8ae9355
fix(io_uring): name the error a POLL_ADD error band carries
sgerbino Sep 1, 2026
7f3c43e
fix(select): reject an out-of-range accept with EMFILE
sgerbino Sep 1, 2026
5e649e7
fix(reactor): do not fault a healthy read on select's out-of-band set
sgerbino Sep 1, 2026
72f550f
fix(iocp): keep the forward resolver alive across its async completion
sgerbino Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ jobs:
cd boost-root
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTING=ON \
-DBOOST_INCLUDE_LIBRARIES="${{ steps.patch.outputs.module }}" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build --target tests -j$(sysctl -n hw.ncpu)
Expand Down
72 changes: 66 additions & 6 deletions doc/error-handling-rulebook.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ by classifying its failures:
would be an attractive nuisance: the retry it invites is a
double-close hazard, because POSIX leaves descriptor release under
`EINTR` unspecified.
- **No caller at all** — an internal wakeup (`interrupt_reactor()`,
`wake_self()`) has nobody to report to, but swallowing the failure
must not swallow every later wake too: a coalescing flag stands for
a byte a failed write never sent, so the failure path disarms it.
The cost is then the wakes already in flight rather than every wake
after them. Never throw from a wake path.
- A service's `shutdown()` is the same shape. Handing process-wide
state back (`sigaction`/`signal` restored to `SIG_DFL`) has no
channel and no caller left to act on one, so its result is
`std::ignore`d deliberately.
- Never both channels for one operation. Never `std::error_code&`
out-params. Never a throwing/non-throwing overload pair.

Expand Down Expand Up @@ -62,6 +72,17 @@ as `std::system_error` carrying the code the piecewise spelling
returns); and root setup for which no code-returning spelling can
exist (`io_context` backend creation, allocation).

Root setup is everything the backend needs — completion port, ring,
wakeup channel, thread pool — and all of it is built during
construction, so a system that refuses any of it throws from the
constructor instead of from the first operation, and the failed
construction leaves nothing open. An initiator may then assume that
infrastructure exists, which is what makes "initiators never throw"
reachable at all. The one piece held back is the thread pool's
workers, which start on the first blocking operation: a thread the
system refuses there is reported through that operation's own channel,
never thrown.

## 3. The Classification Test

Ask: **can the caller reliably prevent the failure by checking state
Expand Down Expand Up @@ -125,6 +146,23 @@ second channel:
- `tls_context` setters record configuration that is applied when a
handshake first configures the engine; application failures surface
through that handshake's completion.
- A failure with no operation to attach to is latched on the object
and pre-answers the operations that follow, until the step it
belongs to succeeds again: a multishot arming the ring never took
reports to every accept until the next arming clears it.
- A completion queued into a scheduler that spends a
`work_finished()` on everything it dispatches needs a matching
`work_started()`. An operation nothing counted reports through its
owner's channel instead of the completion queue.
- Infrastructure an operation needs but does not own — the
blocking-I/O pool's worker threads, the wait reactor's polling
thread — is created on the first operation that needs it, and a
system that refuses it answers through that operation's completion.
The refusal is that operation's alone: the next one asks again. A
refusal the initiator learns of on its own thread, before any part of
the operation is cross-thread, completes there — the same exit the
closed-object and zero-length contracts take a few lines above it,
not a completion posted back through the scheduler.

## 6. Attributes and Spelling

Expand All @@ -139,8 +177,11 @@ second channel:
functions with a `-DBOOST_COROSIO_DYN_LINK -DBOOST_COROSIO_SOURCE`
syntax check.
- Deliberate discards use `std::ignore = expr;`, never `(void)`
casts. Reserve them for calls whose outcome is asserted downstream
(hostile-input tests, best-effort bench teardown).
casts and never `[[maybe_unused]]` on a named result. Reserve them
for calls whose outcome is asserted downstream (hostile-input tests,
best-effort bench teardown), whose failure is impossible by
construction at that site, or which have no channel to report on at
all (§1's wake and `shutdown()` paths).
- Unused names — parameters kept for signature clarity, structured
bindings partially consumed, `#if`-gated uses — are declared
`[[maybe_unused]]`, never silenced with a void cast.
Expand All @@ -157,14 +198,20 @@ second channel:
`filename_too_long`,
`already_connected` (`connect_pair` on an open socket),
`no_such_device_or_address` (`corosio::connect` with no viable
candidate).
candidate),
`resource_unavailable_try_again` (io_uring submission queue
exhausted, for a submitted op, for the signal reader, and for a
multishot arming that never reached the kernel, latched on the
object until an arming succeeds; and a polling thread the system
would not start).
- Portable comparison comes from **normalizing at the boundary**: the
Windows `make_err` maps the contracted WSA/Win32 codes to
generic-category `errc` values (`WSAEOPNOTSUPP`, `WSAENOTSOCK`,
`WSAEAFNOSUPPORT`, `WSAEPROTOTYPE`, `WSAEADDRINUSE`,
`WSAEADDRNOTAVAIL`, `ERROR_NEGATIVE_SEEK`; `iocp_make_err` adds the
async condition set and `WSAEBADF`/`ERROR_INVALID_HANDLE`). On
POSIX, raw errno satisfies `errc` comparison with one exception:
`WSAEADDRNOTAVAIL`, `ERROR_NEGATIVE_SEEK`, `ERROR_MAX_THRDS_REACHED`;
`iocp_make_err` adds the async condition set and
`WSAEBADF`/`ERROR_INVALID_HANDLE`). On POSIX, raw errno satisfies
`errc` comparison with one exception:
`make_err` normalizes `ENOTSUP` so platforms where it differs from
`EOPNOTSUPP` still compare equal to
`errc::operation_not_supported`.
Expand All @@ -175,10 +222,23 @@ second channel:
AF_INET sockets; Darwin's `getsockopt(TCP_NODELAY)` on AF_UNIX);
Windows reports `not_a_socket` where POSIX validation reports
`EBADF` for garbage (non-sentinel) handles.
- A failing call that leaves a zero last error must not become an
empty `error_code`: read the last error before anything that can
clobber it, and substitute rather than report success. Prefer a
contracted condition to a plausible-looking raw platform value,
which is indistinguishable from a code the provider really gave.
- Conditions the standard cannot spell come from capy:
`capy::cond::eof`, `capy::cond::canceled` (a stop token, not
`errc::operation_canceled`), `capy::cond::timeout` (our deadline,
not a kernel `ETIMEDOUT`).
- A background thread that dies mid-flight owes one answer, not two:
the error that killed it, latched, both to the operations it was
holding and to the ones that arrive afterwards. `canceled` is a
stop token and belongs only to operations something cancelled — a
thread that never started is the same rule from the other end: the
operation that asked for it reports the code the system gave
(`resource_unavailable_try_again` where the refusal carries no code
of its own), never `canceled`.

## 8. Testing

Expand Down
8 changes: 6 additions & 2 deletions doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@ On POSIX platforms, file I/O (`stream_file`, `random_access_file`)
and DNS resolution use a shared thread pool.

* *Concurrent file operations*: increase to match expected
parallelism (e.g. 4 for four concurrent file reads).
* *No file I/O*: leave at 1 (the pool is created lazily).
parallelism (e.g. 4 for four concurrent file reads). The whole set
starts at once, on the first file or resolver call, so a larger pool
makes that one call more expensive and no other.
* *No file I/O*: leave at 1; the pool is created with the context, but
its workers start on the first file or resolver operation, so a pool
nothing uses costs nothing.

[#single-threaded-mode]
=== Locking Tiers (`locking`)
Expand Down
Loading
Loading