Conversation
9cb5378 to
27c33fc
Compare
📝 WalkthroughWalkthroughAdds a BSD kqueue-based asynchronous I/O backend: kqueue_context, a kqueue_scheduler reactor, kqueue operation types, kqueue socket and acceptor services, and switches io_context to alias kqueue_context when BOOST_COROSIO_HAS_KQUEUE is defined. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #114 +/- ##
===========================================
- Coverage 79.96% 79.70% -0.27%
===========================================
Files 65 65
Lines 5661 5661
===========================================
- Hits 4527 4512 -15
- Misses 1134 1149 +15 see 2 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
include/boost/corosio/io_context.hpp (1)
47-48:⚠️ Potential issue | 🟡 MinorStale
[future]tags in the docstring.Line 47 says
kqueue_context (kqueue) [future]and line 48 saysselect_context (select) [future], but both are now implemented. These tags should be removed to avoid confusion.Proposed fix
- - BSD/macOS: `kqueue_context` (kqueue) [future] - - Other POSIX: `select_context` (select) [future] + - BSD/macOS: `kqueue_context` (kqueue) + - Other POSIX: `select_context` (select)
🤖 Fix all issues with AI agents
In `@src/corosio/src/detail/kqueue/acceptors.cpp`:
- Around line 192-196: The coroutine resume is using saved_ex.dispatch(saved_h)
which differs from other acceptor ops that call resume_coro(saved_ex, saved_h);
change the resume call in this scope to use resume_coro(saved_ex, saved_h)
instead of saved_ex.dispatch(saved_h), preserving the existing move of ex and h
(saved_ex and saved_h) and keeping prevent_premature_destruction (impl_ptr)
alive; mirror the pattern used in kqueue_op::operator()() and
kqueue_connect_op::operator()() so any extra behavior in resume_coro is applied
consistently.
In `@src/corosio/src/detail/kqueue/scheduler.cpp`:
- Around line 982-983: The throw from throw_system_error can unwind through
run_task and leave task_running_ stuck true; modify do_one so the call to
run_task (the section that sets task_running_ around line 1100) is protected by
a scope guard (RAII or try/finally) that will clear task_running_ on exit
whether run_task returns normally or throws; ensure the guard is constructed
immediately after task_running_ is set and that its destructor (or
catch/finally) resets task_running_ = false so task_running_ cannot remain true
if an exception from kevent propagates.
🧹 Nitpick comments (11)
src/corosio/src/detail/kqueue/acceptors.hpp (2)
119-119: Consider usingnative_handle_typefor consistency with the socket impl.
kqueue_socket_impl::native_handle()(insockets.hppline 118) returnsnative_handle_type, while this acceptor accessor returns rawint. Although this is a non-virtual, non-override accessor on an internal type, using the same typedef would be more consistent across the kqueue backend.
194-201: Move operations are implicitly deleted but not explicitly declared.
kqueue_context(line 81-82 ofkqueue_context.hpp) explicitly deletes both copy and move. Here only copy is deleted; move is implicitly deleted due to the deleted copy. Consider adding explicit= deletefor move for consistency and clarity.src/corosio/src/detail/kqueue/sockets.hpp (3)
171-184: Encapsulation inconsistency withkqueue_acceptor_state.
kqueue_acceptor_state(inacceptors.hpplines 172-187) uses private members withfriend class kqueue_acceptor_service, whilekqueue_socket_stateexposes all members publicly. Consider aligning the two for consistency — either both usefriendor both use public access.
142-142:set_sockethas no precondition guard against leaking an existing fd.If
fd_is already valid (≥ 0) whenset_socketis called, the old fd leaks. Currently this is only called fromacceptaftercreate_impl()wherefd_is-1, but a defensive check or assertion would prevent future misuse.Proposed fix
- void set_socket(int fd) noexcept { fd_ = fd; } + void set_socket(int fd) noexcept + { + BOOST_ASSERT(fd_ < 0 && "set_socket called on already-open socket"); + fd_ = fd; + }
191-198: Same optional nit as the acceptor service: explicit move deletion for consistency.
kqueue_contextdeletes both copy and move explicitly. This service only deletes copy; move is implicitly deleted. Consider making it explicit.src/corosio/src/kqueue_context.cpp (1)
44-45: Narrowingunsigned→intfor the concurrency hint.
static_cast<int>(concurrency_hint)would produce implementation-defined behavior if the value exceedsINT_MAX. This is practically unreachable via the default constructor (hardware thread counts won't hitINT_MAX), but an explicit user-supplied hint could theoretically trigger it. A clamp or assertion would make the contract clear.Proposed defensive clamp
- sched_ = &make_service<detail::kqueue_scheduler>( - static_cast<int>(concurrency_hint)); + sched_ = &make_service<detail::kqueue_scheduler>( + static_cast<int>(std::min(concurrency_hint, + static_cast<unsigned>(std::numeric_limits<int>::max()))));src/corosio/src/detail/kqueue/sockets.cpp (1)
298-355: Repeated EAGAIN park-retry-cancel pattern across do_read_io/do_write_io.The
work_started() → check read_ready → park or retry → cancellation checkpattern at lines 298–355 and 382–438 is structurally identical to the one inconnect()andacceptors.cpp::accept(). Each differs only in whichdesc_state_slot is used (read_op,write_op,connect_op).This isn't a correctness issue, but consider extracting a helper template or function that takes the slot pointer as a parameter to reduce the ~50-line duplication per call site. Not urgent for this PR.
Also applies to: 382-438
src/corosio/src/detail/kqueue/acceptors.cpp (2)
100-132: Accepted socket setup inoperator()duplicatesperform_iosocket configuration.
kqueue_accept_op::perform_io()inop.hpp(lines 392–419) already sets non-blocking, close-on-exec, and SO_NOSIGPIPE on the accepted fd. Thenoperator()()here at lines 121–132 sets SO_NOSIGPIPE again. For the synchronous accept path (whereperform_ioisn't called),operator()is the only place these are set — but non-blocking and cloexec are set inaccept()at lines 239–257, while SO_NOSIGPIPE is deferred tooperator().This split means socket flag setup is spread across three locations. Consider centralizing the accepted-fd configuration (non-blocking, cloexec, SO_NOSIGPIPE) into a single helper to avoid divergence.
Example helper
// In a shared header or anonymous namespace: inline std::error_code configure_accepted_fd(int fd) noexcept { int flags = ::fcntl(fd, F_GETFL, 0); if (flags == -1 || ::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) return make_err(errno); if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) return make_err(errno); int one = 1; if (::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) == -1) return make_err(errno); return {}; }This could then be used in
perform_io(),accept()sync path, andoperator()().
561-567:socket_service()usesdynamic_cast— consider performance for hot paths.
dynamic_castat line 566 involves RTTI. Ifsocket_service()is called per-accept (which it is, at line 104–106 of operator()), this happens on every accepted connection. In most builds, RTTI overhead is negligible, but for high-throughput acceptors, a cached pointer could avoid the repeated cast.src/corosio/src/detail/kqueue/op.hpp (2)
382-428:kqueue_accept_op::perform_ioconfigures the accepted fd inline, duplicatingaccept()andoperator().As noted in the acceptors.cpp review, the accepted fd configuration (non-blocking, cloexec, SO_NOSIGPIPE) is performed here in
perform_io(), and again partially inacceptors.cpp::accept()(sync path) andoperator()(). Sinceoperator()always runs afterperform_io(), the SO_NOSIGPIPE inoperator()is redundant for the async path.Consider whether
perform_io()should handle ALL fd configuration (removing it fromoperator()), or whether a shared helper should be used. This would consolidate the logic and prevent inconsistencies if requirements change.
30-43: Platform-specific headers in an.hppfile undersrc/.Lines 30–43 include
<unistd.h>,<errno.h>,<fcntl.h>,<netinet/in.h>,<sys/socket.h>, and<sys/uio.h>. This is permitted since the file is undersrc/and guarded byBOOST_COROSIO_HAS_KQUEUE, but these headers are included becauseperform_io()implementations are defined inline in this header. If any of these ops'perform_io()were moved to.cppfiles, the POSIX includes could be removed from the header.Not blocking — just noting the trade-off between inline performance and header hygiene.
|
An automated preview of the documentation is available at https://114.corosio.prtest3.cppalliance.org/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-02-09 21:41:20 UTC |
27c33fc to
6e75b88
Compare
|
GCOVR code coverage report https://114.corosio.prtest3.cppalliance.org/gcovr/index.html Build time: 2026-02-09 21:47:06 UTC |
794b9cb to
17f4bcf
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/corosio/src/detail/kqueue/scheduler.cpp`:
- Around line 754-771: The kevent() call in
kqueue_scheduler::interrupt_reactor() currently ignores its return value and can
silently fail; update interrupt_reactor() to check kevent(...)'s return (and/or
errno) and handle failures by emitting a diagnostic (e.g., debug log or assert)
including errno/message so a failed trigger is visible; locate the kevent
invocation near EV_SET(&ev, 0, EVFILT_USER, 0, NOTE_TRIGGER, 0, nullptr) and
user_event_armed_/kq_fd_ and add a conditional that logs/asserts when kevent
returns -1 (or unexpected value) with the error details.
- Around line 1054-1069: The race is that
completed_ops_.splice(ctx->private_queue) can expose work before
outstanding_work_ is incremented; update outstanding_work_ by adding
ctx->private_outstanding_work (and then zero ctx->private_outstanding_work)
before calling completed_ops_.splice(ctx->private_queue) so the global counter
reflects the incoming work (matching the drain_thread_queue pattern), then
proceed to splice and signal via maybe_unlock_and_signal_one; this prevents
task_cleanup's destructor later from double-counting and avoids premature
stop()/interrupt_reactor() when work_finished() runs concurrently.
In `@src/corosio/src/detail/kqueue/sockets.cpp`:
- Around line 361-443: kqueue_socket_impl::do_write_io() must explicitly handle
the case when ::writev returns 0 to avoid using a stale errno; add an early
check after ssize_t n = ::writev(...) to detect n == 0 and treat it as a closed
peer (e.g. call op.complete(EPIPE, 0); svc_.post(&op); return;), mirroring the
do_read_io() pattern so we never branch on an indeterminate errno value.
🧹 Nitpick comments (7)
src/corosio/src/detail/kqueue/acceptors.cpp (2)
83-197: Accept completion handler: missingderegister_descriptoronSO_NOSIGPIPEfailure path.When
SO_NOSIGPIPEfails (line 124), the code closes the fd (line 128) and destroys the impl (line 130), but doesn't explicitly callderegister_descriptorbefore closing. While kqueue auto-removes closed fds, there's a brief window where the reactor could deliver events for this fd to thedescriptor_statethat's about to be destroyed. In practice this is safe becausedestroy_impldrops the lastshared_ptrand no ops are parked, but addingderegister_descriptorbefore::close()would be more defensive.Suggested fix
if (::setsockopt(accepted_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) == -1) { if (ec_out) *ec_out = make_err(errno); + socket_svc->scheduler().deregister_descriptor(accepted_fd); ::close(accepted_fd); accepted_fd = -1; socket_svc->destroy_impl(impl);
234-268: Consider usingaccept4()on FreeBSD to avoid the fcntl TOCTOU window.The comment on line 234 notes FreeBSD supports
accept4()withSOCK_NONBLOCK | SOCK_CLOEXEC. Using it (with a compile-time or runtime check) would atomically set both flags, eliminating thefcntlTOCTOU window and the three extra syscalls. This could be deferred to a follow-up optimization.src/corosio/src/detail/kqueue/op.hpp (1)
382-428:SO_NOSIGPIPEis set redundantly for the async accept path.
perform_io()setsSO_NOSIGPIPEon the accepted fd (lines 412–419), butkqueue_accept_op::operator()()inacceptors.cpp(lines 122–133) sets it again on the same fd. The synchronous accept path inaccept()(acceptors.cpp lines 237–268) only relies onoperator()()for this. The duplicate inperform_io()is harmless (setsockopt is idempotent) but wastes a syscall.Consider removing
SO_NOSIGPIPEfromperform_io()and relying solely onoperator()()for consistency with the synchronous path, or removing it fromoperator()()and adding it to the synchronous path — either way, setting it in exactly one place.src/corosio/src/detail/kqueue/sockets.hpp (2)
148-162: Public internal members are documented but could benefit from encapsulation.The
conn_,rd_,wr_,desc_state_, and initiator members are public with a clear comment explaining they're for internal reactor/scheduler integration. Consider making them private withfriend class kqueue_scheduler; friend struct descriptor_state;to prevent accidental misuse, though the current approach is acceptable for an internaldetailclass.
171-184:kqueue_socket_statedata members are public without a friend declaration.Unlike
kqueue_acceptor_state(which usesfriend class kqueue_acceptor_service+private:data),kqueue_socket_stateexposessched_,mutex_,socket_list_, andsocket_ptrs_as public. For consistency with the acceptor counterpart, consider making these private with a friend declaration.Suggested change
class kqueue_socket_state { + friend class kqueue_socket_service; + public: explicit kqueue_socket_state(kqueue_scheduler& sched) noexcept : sched_(sched) { } +private: kqueue_scheduler& sched_; std::mutex mutex_; intrusive_list<kqueue_socket_impl> socket_list_; std::unordered_map<kqueue_socket_impl*, std::shared_ptr<kqueue_socket_impl>> socket_ptrs_; };src/corosio/src/detail/kqueue/scheduler.cpp (2)
906-930:work_cleanupmay leave lock in an unexpected state for the caller.Line 920 re-acquires the lock if the private queue is non-empty, but does not re-acquire it if the queue is empty. The caller (
do_oneat Line 1132) returns immediately afterwork_cleanupdestructs, andrun()at Line 570 conditionally re-locks withif (!lock.owns_lock()). This works but creates a subtle contract:work_cleanupmay or may not leave the lock held, anddo_one's caller must always check. The existing code handles this correctly, but a brief comment at Line 920 noting "lock left held for do_one's caller to check" would help future maintainers.
440-448: Use-after-free pattern is safe but fragile — consider a comment or alternative.Line 442 copies the coroutine handle, Line 443 deletes
this, then Line 447 issues anatomic_thread_fence. The existing comment on the fence is good, but thedelete this+ use-local-copy pattern could trip up future maintainers. The comment focuses on the fence semantics but doesn't call out that thedelete thisis intentional and safe becausehis a local copy. A one-liner like// Safe: h is a stack copy; 'this' is no longer accessed after delete.would help.
17f4bcf to
515ae24
Compare
Summary by CodeRabbit