LCOV - code coverage report
Current view: top level - corosio/native/detail/select - select_scheduler.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 87.8 % 164 144 20
Test Date: 2026-03-30 19:32:49 Functions: 100.0 % 11 11

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2026 Steve Gerbino
       3                 : //
       4                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       5                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       6                 : //
       7                 : // Official repository: https://github.com/cppalliance/corosio
       8                 : //
       9                 : 
      10                 : #ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_SCHEDULER_HPP
      11                 : #define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_SCHEDULER_HPP
      12                 : 
      13                 : #include <boost/corosio/detail/platform.hpp>
      14                 : 
      15                 : #if BOOST_COROSIO_HAS_SELECT
      16                 : 
      17                 : #include <boost/corosio/detail/config.hpp>
      18                 : #include <boost/capy/ex/execution_context.hpp>
      19                 : 
      20                 : #include <boost/corosio/native/detail/reactor/reactor_scheduler.hpp>
      21                 : 
      22                 : #include <boost/corosio/native/detail/select/select_op.hpp>
      23                 : #include <boost/corosio/detail/timer_service.hpp>
      24                 : #include <boost/corosio/native/detail/make_err.hpp>
      25                 : #include <boost/corosio/native/detail/posix/posix_resolver_service.hpp>
      26                 : #include <boost/corosio/native/detail/posix/posix_signal_service.hpp>
      27                 : #include <boost/corosio/native/detail/posix/posix_stream_file_service.hpp>
      28                 : #include <boost/corosio/native/detail/posix/posix_random_access_file_service.hpp>
      29                 : 
      30                 : #include <boost/corosio/detail/except.hpp>
      31                 : 
      32                 : #include <sys/select.h>
      33                 : #include <unistd.h>
      34                 : #include <errno.h>
      35                 : #include <fcntl.h>
      36                 : 
      37                 : #include <atomic>
      38                 : #include <chrono>
      39                 : #include <cstdint>
      40                 : #include <limits>
      41                 : #include <mutex>
      42                 : #include <unordered_map>
      43                 : 
      44                 : namespace boost::corosio::detail {
      45                 : 
      46                 : struct select_op;
      47                 : struct select_descriptor_state;
      48                 : 
      49                 : /** POSIX scheduler using select() for I/O multiplexing.
      50                 : 
      51                 :     This scheduler implements the scheduler interface using the POSIX select()
      52                 :     call for I/O event notification. It inherits the shared reactor threading
      53                 :     model from reactor_scheduler_base: signal state machine, inline completion
      54                 :     budget, work counting, and the do_one event loop.
      55                 : 
      56                 :     The design mirrors epoll_scheduler for behavioral consistency:
      57                 :     - Same single-reactor thread coordination model
      58                 :     - Same deferred I/O pattern (reactor marks ready; workers do I/O)
      59                 :     - Same timer integration pattern
      60                 : 
      61                 :     Known Limitations:
      62                 :     - FD_SETSIZE (~1024) limits maximum concurrent connections
      63                 :     - O(n) scanning: rebuilds fd_sets each iteration
      64                 :     - Level-triggered only (no edge-triggered mode)
      65                 : 
      66                 :     @par Thread Safety
      67                 :     All public member functions are thread-safe.
      68                 : */
      69                 : class BOOST_COROSIO_DECL select_scheduler final : public reactor_scheduler_base
      70                 : {
      71                 : public:
      72                 :     /** Construct the scheduler.
      73                 : 
      74                 :         Creates a self-pipe for reactor interruption.
      75                 : 
      76                 :         @param ctx Reference to the owning execution_context.
      77                 :         @param concurrency_hint Hint for expected thread count (unused).
      78                 :     */
      79                 :     select_scheduler(capy::execution_context& ctx, int concurrency_hint = -1);
      80                 : 
      81                 :     /// Destroy the scheduler.
      82                 :     ~select_scheduler() override;
      83                 : 
      84                 :     select_scheduler(select_scheduler const&)            = delete;
      85                 :     select_scheduler& operator=(select_scheduler const&) = delete;
      86                 : 
      87                 :     /// Shut down the scheduler, draining pending operations.
      88                 :     void shutdown() override;
      89                 : 
      90                 :     /** Return the maximum file descriptor value supported.
      91                 : 
      92                 :         Returns FD_SETSIZE - 1, the maximum fd value that can be
      93                 :         monitored by select(). Operations with fd >= FD_SETSIZE
      94                 :         will fail with EINVAL.
      95                 : 
      96                 :         @return The maximum supported file descriptor value.
      97                 :     */
      98                 :     static constexpr int max_fd() noexcept
      99                 :     {
     100                 :         return FD_SETSIZE - 1;
     101                 :     }
     102                 : 
     103                 :     /** Register a descriptor for persistent monitoring.
     104                 : 
     105                 :         The fd is added to the registered_descs_ map and will be
     106                 :         included in subsequent select() calls. The reactor is
     107                 :         interrupted so a blocked select() rebuilds its fd_sets.
     108                 : 
     109                 :         @param fd The file descriptor to register.
     110                 :         @param desc Pointer to descriptor state for this fd.
     111                 :     */
     112                 :     void register_descriptor(int fd, select_descriptor_state* desc) const;
     113                 : 
     114                 :     /** Deregister a persistently registered descriptor.
     115                 : 
     116                 :         @param fd The file descriptor to deregister.
     117                 :     */
     118                 :     void deregister_descriptor(int fd) const;
     119                 : 
     120                 :     /** Interrupt the reactor so it rebuilds its fd_sets.
     121                 : 
     122                 :         Called when a write or connect op is registered after
     123                 :         the reactor's snapshot was taken. Without this, select()
     124                 :         may block not watching for writability on the fd.
     125                 :     */
     126                 :     void notify_reactor() const;
     127                 : 
     128                 : private:
     129                 :     void
     130                 :     run_task(std::unique_lock<std::mutex>& lock, context_type* ctx,
     131                 :         long timeout_us) override;
     132                 :     void interrupt_reactor() const override;
     133                 :     long calculate_timeout(long requested_timeout_us) const;
     134                 : 
     135                 :     // Self-pipe for interrupting select()
     136                 :     int pipe_fds_[2]; // [0]=read, [1]=write
     137                 : 
     138                 :     // Per-fd tracking for fd_set building
     139                 :     mutable std::unordered_map<int, select_descriptor_state*> registered_descs_;
     140                 :     mutable int max_fd_ = -1;
     141                 : };
     142                 : 
     143 HIT         195 : inline select_scheduler::select_scheduler(capy::execution_context& ctx, int)
     144             195 :     : pipe_fds_{-1, -1}
     145             195 :     , max_fd_(-1)
     146                 : {
     147             195 :     if (::pipe(pipe_fds_) < 0)
     148 MIS           0 :         detail::throw_system_error(make_err(errno), "pipe");
     149                 : 
     150 HIT         585 :     for (int i = 0; i < 2; ++i)
     151                 :     {
     152             390 :         int flags = ::fcntl(pipe_fds_[i], F_GETFL, 0);
     153             390 :         if (flags == -1)
     154                 :         {
     155 MIS           0 :             int errn = errno;
     156               0 :             ::close(pipe_fds_[0]);
     157               0 :             ::close(pipe_fds_[1]);
     158               0 :             detail::throw_system_error(make_err(errn), "fcntl F_GETFL");
     159                 :         }
     160 HIT         390 :         if (::fcntl(pipe_fds_[i], F_SETFL, flags | O_NONBLOCK) == -1)
     161                 :         {
     162 MIS           0 :             int errn = errno;
     163               0 :             ::close(pipe_fds_[0]);
     164               0 :             ::close(pipe_fds_[1]);
     165               0 :             detail::throw_system_error(make_err(errn), "fcntl F_SETFL");
     166                 :         }
     167 HIT         390 :         if (::fcntl(pipe_fds_[i], F_SETFD, FD_CLOEXEC) == -1)
     168                 :         {
     169 MIS           0 :             int errn = errno;
     170               0 :             ::close(pipe_fds_[0]);
     171               0 :             ::close(pipe_fds_[1]);
     172               0 :             detail::throw_system_error(make_err(errn), "fcntl F_SETFD");
     173                 :         }
     174                 :     }
     175                 : 
     176 HIT         195 :     timer_svc_ = &get_timer_service(ctx, *this);
     177             195 :     timer_svc_->set_on_earliest_changed(
     178            2637 :         timer_service::callback(this, [](void* p) {
     179            2442 :             static_cast<select_scheduler*>(p)->interrupt_reactor();
     180            2442 :         }));
     181                 : 
     182             195 :     get_resolver_service(ctx, *this);
     183             195 :     get_signal_service(ctx, *this);
     184             195 :     get_stream_file_service(ctx, *this);
     185             195 :     get_random_access_file_service(ctx, *this);
     186                 : 
     187             195 :     completed_ops_.push(&task_op_);
     188             195 : }
     189                 : 
     190             390 : inline select_scheduler::~select_scheduler()
     191                 : {
     192             195 :     if (pipe_fds_[0] >= 0)
     193             195 :         ::close(pipe_fds_[0]);
     194             195 :     if (pipe_fds_[1] >= 0)
     195             195 :         ::close(pipe_fds_[1]);
     196             390 : }
     197                 : 
     198                 : inline void
     199             195 : select_scheduler::shutdown()
     200                 : {
     201             195 :     shutdown_drain();
     202                 : 
     203             195 :     if (pipe_fds_[1] >= 0)
     204             195 :         interrupt_reactor();
     205             195 : }
     206                 : 
     207                 : inline void
     208            4591 : select_scheduler::register_descriptor(
     209                 :     int fd, select_descriptor_state* desc) const
     210                 : {
     211            4591 :     if (fd < 0 || fd >= FD_SETSIZE)
     212 MIS           0 :         detail::throw_system_error(make_err(EINVAL), "select: fd out of range");
     213                 : 
     214 HIT        4591 :     desc->registered_events = reactor_event_read | reactor_event_write;
     215            4591 :     desc->fd                = fd;
     216            4591 :     desc->scheduler_        = this;
     217            4591 :     desc->ready_events_.store(0, std::memory_order_relaxed);
     218                 : 
     219                 :     {
     220            4591 :         std::lock_guard lock(desc->mutex);
     221            4591 :         desc->impl_ref_.reset();
     222            4591 :         desc->read_ready  = false;
     223            4591 :         desc->write_ready = false;
     224            4591 :     }
     225                 : 
     226                 :     {
     227            4591 :         std::lock_guard lock(mutex_);
     228            4591 :         registered_descs_[fd] = desc;
     229            4591 :         if (fd > max_fd_)
     230            4587 :             max_fd_ = fd;
     231            4591 :     }
     232                 : 
     233            4591 :     interrupt_reactor();
     234            4591 : }
     235                 : 
     236                 : inline void
     237            4591 : select_scheduler::deregister_descriptor(int fd) const
     238                 : {
     239            4591 :     std::lock_guard lock(mutex_);
     240                 : 
     241            4591 :     auto it = registered_descs_.find(fd);
     242            4591 :     if (it == registered_descs_.end())
     243 MIS           0 :         return;
     244                 : 
     245 HIT        4591 :     registered_descs_.erase(it);
     246                 : 
     247            4591 :     if (fd == max_fd_)
     248                 :     {
     249            4534 :         max_fd_ = pipe_fds_[0];
     250            8983 :         for (auto& [registered_fd, state] : registered_descs_)
     251                 :         {
     252            4449 :             if (registered_fd > max_fd_)
     253            4440 :                 max_fd_ = registered_fd;
     254                 :         }
     255                 :     }
     256            4591 : }
     257                 : 
     258                 : inline void
     259           17952 : select_scheduler::notify_reactor() const
     260                 : {
     261           17952 :     interrupt_reactor();
     262           17952 : }
     263                 : 
     264                 : inline void
     265           25323 : select_scheduler::interrupt_reactor() const
     266                 : {
     267           25323 :     char byte               = 1;
     268           25323 :     [[maybe_unused]] auto r = ::write(pipe_fds_[1], &byte, 1);
     269           25323 : }
     270                 : 
     271                 : inline long
     272           79141 : select_scheduler::calculate_timeout(long requested_timeout_us) const
     273                 : {
     274           79141 :     if (requested_timeout_us == 0)
     275 MIS           0 :         return 0;
     276                 : 
     277 HIT       79141 :     auto nearest = timer_svc_->nearest_expiry();
     278           79141 :     if (nearest == timer_service::time_point::max())
     279              46 :         return requested_timeout_us;
     280                 : 
     281           79095 :     auto now = std::chrono::steady_clock::now();
     282           79095 :     if (nearest <= now)
     283             315 :         return 0;
     284                 : 
     285                 :     auto timer_timeout_us =
     286           78780 :         std::chrono::duration_cast<std::chrono::microseconds>(nearest - now)
     287           78780 :             .count();
     288                 : 
     289           78780 :     constexpr auto long_max =
     290                 :         static_cast<long long>((std::numeric_limits<long>::max)());
     291                 :     auto capped_timer_us =
     292           78780 :         (std::min)((std::max)(static_cast<long long>(timer_timeout_us),
     293           78780 :                               static_cast<long long>(0)),
     294           78780 :                    long_max);
     295                 : 
     296           78780 :     if (requested_timeout_us < 0)
     297           78774 :         return static_cast<long>(capped_timer_us);
     298                 : 
     299                 :     return static_cast<long>(
     300               6 :         (std::min)(static_cast<long long>(requested_timeout_us),
     301               6 :                    capped_timer_us));
     302                 : }
     303                 : 
     304                 : inline void
     305           91216 : select_scheduler::run_task(
     306                 :     std::unique_lock<std::mutex>& lock, context_type* ctx, long timeout_us)
     307                 : {
     308                 :     long effective_timeout_us =
     309           91216 :         task_interrupted_ ? 0 : calculate_timeout(timeout_us);
     310                 : 
     311                 :     // Snapshot registered descriptors while holding lock.
     312                 :     // Record which fds need write monitoring to avoid a hot loop:
     313                 :     // select is level-triggered so writable sockets (nearly always
     314                 :     // writable) would cause select() to return immediately every
     315                 :     // iteration if unconditionally added to write_fds.
     316                 :     struct fd_entry
     317                 :     {
     318                 :         int fd;
     319                 :         select_descriptor_state* desc;
     320                 :         bool needs_write;
     321                 :     };
     322                 :     fd_entry snapshot[FD_SETSIZE];
     323           91216 :     int snapshot_count = 0;
     324                 : 
     325          281071 :     for (auto& [fd, desc] : registered_descs_)
     326                 :     {
     327          189855 :         if (snapshot_count < FD_SETSIZE)
     328                 :         {
     329          189855 :             std::lock_guard desc_lock(desc->mutex);
     330          189855 :             snapshot[snapshot_count].fd   = fd;
     331          189855 :             snapshot[snapshot_count].desc = desc;
     332          189855 :             snapshot[snapshot_count].needs_write =
     333          189855 :                 (desc->write_op || desc->connect_op);
     334          189855 :             ++snapshot_count;
     335          189855 :         }
     336                 :     }
     337                 : 
     338           91216 :     if (lock.owns_lock())
     339           79141 :         lock.unlock();
     340                 : 
     341           91216 :     task_cleanup on_exit{this, &lock, ctx};
     342                 : 
     343                 :     fd_set read_fds, write_fds, except_fds;
     344         1550672 :     FD_ZERO(&read_fds);
     345         1550672 :     FD_ZERO(&write_fds);
     346         1550672 :     FD_ZERO(&except_fds);
     347                 : 
     348           91216 :     FD_SET(pipe_fds_[0], &read_fds);
     349           91216 :     int nfds = pipe_fds_[0];
     350                 : 
     351          281071 :     for (int i = 0; i < snapshot_count; ++i)
     352                 :     {
     353          189855 :         int fd = snapshot[i].fd;
     354          189855 :         FD_SET(fd, &read_fds);
     355          189855 :         if (snapshot[i].needs_write)
     356            2242 :             FD_SET(fd, &write_fds);
     357          189855 :         FD_SET(fd, &except_fds);
     358          189855 :         if (fd > nfds)
     359           90953 :             nfds = fd;
     360                 :     }
     361                 : 
     362                 :     struct timeval tv;
     363           91216 :     struct timeval* tv_ptr = nullptr;
     364           91216 :     if (effective_timeout_us >= 0)
     365                 :     {
     366           91170 :         tv.tv_sec  = effective_timeout_us / 1000000;
     367           91170 :         tv.tv_usec = effective_timeout_us % 1000000;
     368           91170 :         tv_ptr     = &tv;
     369                 :     }
     370                 : 
     371           91216 :     int ready = ::select(nfds + 1, &read_fds, &write_fds, &except_fds, tv_ptr);
     372                 : 
     373                 :     // EINTR: signal interrupted select(), just retry.
     374                 :     // EBADF: an fd was closed between snapshot and select(); retry
     375                 :     // with a fresh snapshot from registered_descs_.
     376           91216 :     if (ready < 0)
     377                 :     {
     378 MIS           0 :         if (errno == EINTR || errno == EBADF)
     379               0 :             return;
     380               0 :         detail::throw_system_error(make_err(errno), "select");
     381                 :     }
     382                 : 
     383                 :     // Process timers outside the lock
     384 HIT       91216 :     timer_svc_->process_expired();
     385                 : 
     386           91216 :     op_queue local_ops;
     387                 : 
     388           91216 :     if (ready > 0)
     389                 :     {
     390           83608 :         if (FD_ISSET(pipe_fds_[0], &read_fds))
     391                 :         {
     392                 :             char buf[256];
     393           21936 :             while (::read(pipe_fds_[0], buf, sizeof(buf)) > 0)
     394                 :             {
     395                 :             }
     396                 :         }
     397                 : 
     398          262784 :         for (int i = 0; i < snapshot_count; ++i)
     399                 :         {
     400          179176 :             int fd                        = snapshot[i].fd;
     401          179176 :             select_descriptor_state* desc = snapshot[i].desc;
     402                 : 
     403          179176 :             std::uint32_t flags = 0;
     404          179176 :             if (FD_ISSET(fd, &read_fds))
     405           78082 :                 flags |= reactor_event_read;
     406          179176 :             if (FD_ISSET(fd, &write_fds))
     407            2242 :                 flags |= reactor_event_write;
     408          179176 :             if (FD_ISSET(fd, &except_fds))
     409 MIS           0 :                 flags |= reactor_event_error;
     410                 : 
     411 HIT      179176 :             if (flags == 0)
     412           98854 :                 continue;
     413                 : 
     414           80322 :             desc->add_ready_events(flags);
     415                 : 
     416           80322 :             bool expected = false;
     417           80322 :             if (desc->is_enqueued_.compare_exchange_strong(
     418                 :                     expected, true, std::memory_order_release,
     419                 :                     std::memory_order_relaxed))
     420                 :             {
     421           80322 :                 local_ops.push(desc);
     422                 :             }
     423                 :         }
     424                 :     }
     425                 : 
     426           91216 :     lock.lock();
     427                 : 
     428           91216 :     if (!local_ops.empty())
     429           78082 :         completed_ops_.splice(local_ops);
     430           91216 : }
     431                 : 
     432                 : } // namespace boost::corosio::detail
     433                 : 
     434                 : #endif // BOOST_COROSIO_HAS_SELECT
     435                 : 
     436                 : #endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_SCHEDULER_HPP
        

Generated by: LCOV version 2.3