LCOV - code coverage report
Current view: top level - corosio/native/detail/reactor - reactor_descriptor_state.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 90.4 % 104 94 10
Test Date: 2026-10-17 04:01:32 Functions: 100.0 % 5 5

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2026 Steve Gerbino
       3                 : // Copyright (c) 2026 Michael Vandeberg
       4                 : //
       5                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       6                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       7                 : //
       8                 : // Official repository: https://github.com/cppalliance/corosio
       9                 : //
      10                 : 
      11                 : #ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DESCRIPTOR_STATE_HPP
      12                 : #define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DESCRIPTOR_STATE_HPP
      13                 : 
      14                 : #include <boost/corosio/native/detail/reactor/reactor_events.hpp>
      15                 : #include <boost/corosio/native/detail/reactor/reactor_op_base.hpp>
      16                 : #include <boost/corosio/native/detail/reactor/reactor_scheduler.hpp>
      17                 : #include <boost/corosio/detail/ready_queue.hpp>
      18                 : 
      19                 : #include <boost/corosio/detail/conditionally_enabled_mutex.hpp>
      20                 : 
      21                 : #include <atomic>
      22                 : #include <cstdint>
      23                 : #include <memory>
      24                 : 
      25                 : #include <errno.h>
      26                 : #include <sys/socket.h>
      27                 : 
      28                 : namespace boost::corosio::detail {
      29                 : 
      30                 : /** Per-descriptor state shared across reactor backends.
      31                 : 
      32                 :     Tracks pending operations for a file descriptor. The fd is registered
      33                 :     once with the reactor and stays registered until closed. Uses deferred
      34                 :     I/O: the reactor sets ready_events atomically, then enqueues this state.
      35                 :     When popped by the scheduler, invoke_deferred_io() performs I/O under
      36                 :     the mutex and queues completed ops.
      37                 : 
      38                 :     Non-template: uses reactor_op_base pointers so the scheduler and
      39                 :     descriptor_state code exist as a single copy in the binary regardless
      40                 :     of how many backends are compiled in.
      41                 : 
      42                 :     @par Thread Safety
      43                 :     The mutex protects operation pointers and ready flags. ready_events_
      44                 :     and is_enqueued_ are atomic for lock-free reactor access.
      45                 : */
      46                 : struct reactor_descriptor_state
      47                 :     : scheduler_op
      48                 :     , reactor_retirable
      49                 : {
      50                 :     /// Protects operation pointers and ready/cancel flags.
      51                 :     /// Becomes a no-op in single-threaded mode.
      52                 :     conditionally_enabled_mutex mutex{true};
      53                 : 
      54                 :     /// Pending read operation (guarded by `mutex`).
      55                 :     reactor_op_base* read_op = nullptr;
      56                 : 
      57                 :     /// Pending write operation (guarded by `mutex`).
      58                 :     reactor_op_base* write_op = nullptr;
      59                 : 
      60                 :     /// Pending connect operation (guarded by `mutex`).
      61                 :     reactor_op_base* connect_op = nullptr;
      62                 : 
      63                 :     /// Pending wait-for-read operation (guarded by `mutex`).
      64                 :     reactor_op_base* wait_read_op = nullptr;
      65                 : 
      66                 :     /// Pending wait-for-write operation (guarded by `mutex`).
      67                 :     reactor_op_base* wait_write_op = nullptr;
      68                 : 
      69                 :     /// Pending wait-for-error operation (guarded by `mutex`).
      70                 :     reactor_op_base* wait_error_op = nullptr;
      71                 : 
      72                 :     /// True if a read edge event arrived before an op was registered.
      73                 :     bool read_ready = false;
      74                 : 
      75                 :     /// True if a write edge event arrived before an op was registered.
      76                 :     bool write_ready = false;
      77                 : 
      78                 :     /// Event mask set during registration (no mutex needed).
      79                 :     std::uint32_t registered_events = 0;
      80                 : 
      81                 :     /// The reactor refused to watch this fd (e.g. /dev/null on epoll);
      82                 :     /// its I/O never blocks, and an op that would park must not.
      83                 :     bool unpollable = false;
      84                 : 
      85                 :     /// File descriptor this state tracks.
      86                 :     int fd = -1;
      87                 : 
      88                 :     /// Accumulated ready events (set by reactor, read by scheduler).
      89                 :     std::atomic<std::uint32_t> ready_events_{0};
      90                 : 
      91                 :     /// True while this state is queued in the scheduler's completed_ops.
      92                 :     std::atomic<bool> is_enqueued_{false};
      93                 : 
      94                 :     /// Owning scheduler for posting completions.
      95                 :     reactor_scheduler const* scheduler_ = nullptr;
      96                 : 
      97                 :     /// Prevents impl destruction while queued in the scheduler.
      98                 :     detail::object_ref object_ref_;
      99                 : 
     100                 :     /// Add ready events atomically.
     101                 :     /// Release pairs with the consumer's acquire exchange on
     102                 :     /// ready_events_ so the consumer sees all flags. Acquire pairs with
     103                 :     /// registration's release store, so a reactor pass on another
     104                 :     /// thread sees the state registration initialized before handing
     105                 :     /// it to the kernel. On x86 (TSO) this compiles to the same LOCK OR
     106                 :     /// as relaxed.
     107 HIT      124497 :     void add_ready_events(std::uint32_t ev) noexcept
     108                 :     {
     109          124497 :         ready_events_.fetch_or(ev, std::memory_order_acq_rel);
     110          124497 :     }
     111                 : 
     112                 :     /// Invoke deferred I/O and dispatch completions.
     113           54214 :     void operator()() override
     114                 :     {
     115           54214 :         invoke_deferred_io();
     116           54214 :     }
     117                 : 
     118                 :     /// Move @p keep into this state if it is queued without one.
     119           22992 :     void hand_off(object_ref& keep) noexcept override
     120                 :     {
     121                 :         // Locked even to read the flag: a popped invocation clears it at
     122                 :         // the start of its critical section and still uses this state
     123                 :         // until it unlocks, so an unlocked false does not mean idle.
     124           22992 :         conditionally_enabled_mutex::scoped_lock lock(mutex);
     125           22992 :         if (is_enqueued_.load(std::memory_order_acquire) && !object_ref_)
     126             886 :             object_ref_ = std::move(keep);
     127           22992 :     }
     128                 : 
     129                 :     /// Destroy without invoking.
     130                 :     /// Called during scheduler::shutdown() drain. Clear object_ref_ to break
     131                 :     /// the self-referential cycle set by close_socket().
     132             228 :     void destroy() override
     133                 :     {
     134             228 :         object_ref_.reset();
     135             228 :     }
     136                 : 
     137                 :     /** Perform deferred I/O and queue completions.
     138                 : 
     139                 :         Performs I/O under the mutex and queues completed ops. EAGAIN
     140                 :         ops stay parked in their slot for re-delivery on the next
     141                 :         edge event.
     142                 :     */
     143                 :     void invoke_deferred_io();
     144                 : };
     145                 : 
     146                 : inline void
     147           54216 : reactor_descriptor_state::invoke_deferred_io()
     148                 : {
     149           54216 :     detail::object_ref prevent_impl_destruction;
     150           54216 :     ready_queue local_ops;
     151                 :     // Read under the lock: once it drops, a retired owner may be
     152                 :     // recycled, so nothing below touches this state.
     153           54216 :     reactor_scheduler const* sched = nullptr;
     154                 : 
     155                 :     {
     156           54216 :         conditionally_enabled_mutex::scoped_lock lock(mutex);
     157           54216 :         sched = scheduler_;
     158                 : 
     159                 :         // Must clear is_enqueued_ and move object_ref_ under the same
     160                 :         // lock that processes I/O. close_socket() checks is_enqueued_
     161                 :         // under this mutex — without atomicity between the flag store
     162                 :         // and the ref move, close_socket() could see is_enqueued_==false,
     163                 :         // skip setting object_ref_, and destroy the impl under us.
     164           54216 :         prevent_impl_destruction = std::move(object_ref_);
     165           54216 :         is_enqueued_.store(false, std::memory_order_release);
     166                 : 
     167           54216 :         std::uint32_t ev = ready_events_.exchange(0, std::memory_order_acquire);
     168           54216 :         if (ev == 0)
     169                 :         {
     170                 :             // Mutex unlocks here; compensate for work_cleanup's decrement
     171 MIS           0 :             scheduler_->compensating_work_started();
     172               0 :             return;
     173                 :         }
     174                 : 
     175 HIT       54216 :         int err = 0;
     176           54216 :         if (ev & reactor_event_error)
     177                 :         {
     178                 :             // Force the read/write dispatch below to run: an
     179                 :             // edge-triggered EPOLLERR can arrive alone, and without this
     180                 :             // a parked op never calls perform_io() and, the edge being
     181                 :             // one-shot, never gets another chance -- a permanent hang.
     182                 :             // Every parked op then re-runs its own syscall or probe.
     183                 :             //
     184                 :             // Assumes at least one parked op's own syscall makes
     185                 :             // non-EAGAIN progress; if every op re-parks with EAGAIN this
     186                 :             // sticky error is never redelivered and they hang. No such
     187                 :             // case is known -- a future descriptor type that hits one
     188                 :             // should be handled here.
     189             365 :             ev |= reactor_event_read | reactor_event_write;
     190                 : 
     191                 :             // SO_ERROR clears on read, so take it only for a parked op
     192                 :             // that reports it. A readiness wait reports readiness and
     193                 :             // leaves the error for the next read or write to name, as
     194                 :             // asio does; reading it here with nothing to report it to
     195                 :             // would turn a reset into a clean EOF.
     196             365 :             bool const reports_error =
     197             365 :                 read_op || write_op || connect_op || wait_error_op;
     198             365 :             socklen_t len = sizeof(err);
     199             395 :             if (reports_error &&
     200              30 :                 ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
     201                 :             {
     202                 :                 // Non-socket fd (pipe, chardev, ...): no SO_ERROR, so
     203                 :                 // let the op's own syscall name the real failure.
     204              11 :                 err = (errno == ENOTSOCK) ? 0 : errno;
     205                 :             }
     206                 :             // select raises its exceptional set for out-of-band/urgent
     207                 :             // data as well as for genuine faults; on a healthy socket the
     208                 :             // probe then reads SO_ERROR == 0. Faulting a pending read or
     209                 :             // write on that is wrong, so an I/O operation completes only
     210                 :             // on a real (non-zero) error. wait(error) still names a code
     211                 :             // below.
     212                 :         }
     213                 : 
     214           54216 :         if (ev & reactor_event_read)
     215                 :         {
     216           22332 :             if (read_op)
     217                 :             {
     218            7702 :                 auto* rd = read_op;
     219            7702 :                 if (err)
     220               6 :                     rd->complete(err, 0);
     221                 :                 else
     222            7696 :                     rd->perform_io();
     223                 : 
     224            7702 :                 if (rd->errn == EAGAIN || rd->errn == EWOULDBLOCK)
     225                 :                 {
     226               5 :                     rd->errn = 0;
     227                 :                 }
     228                 :                 else
     229                 :                 {
     230            7697 :                     read_op = nullptr;
     231            7697 :                     local_ops.push(rd);
     232                 :                 }
     233                 :             }
     234                 :             else
     235                 :             {
     236           14630 :                 read_ready = true;
     237                 :             }
     238                 : 
     239                 :             // The event does not prove the socket is still readable: a
     240                 :             // parked read op above may have drained it, or a speculative
     241                 :             // read consumed the data before this dispatch ran. The wait
     242                 :             // op's perform_io() re-probes and reports EAGAIN to stay
     243                 :             // parked.
     244           22332 :             if (wait_read_op)
     245                 :             {
     246              38 :                 auto* wo = wait_read_op;
     247              38 :                 wo->perform_io();
     248                 : 
     249              38 :                 if (wo->errn == EAGAIN || wo->errn == EWOULDBLOCK)
     250                 :                 {
     251               2 :                     wo->errn = 0;
     252                 :                 }
     253                 :                 else
     254                 :                 {
     255              36 :                     wait_read_op = nullptr;
     256              36 :                     local_ops.push(wo);
     257                 :                 }
     258                 :             }
     259                 :         }
     260           54216 :         if (ev & reactor_event_write)
     261                 :         {
     262           46690 :             bool had_write_op = (connect_op || write_op);
     263                 :             // A writable event on a socket still in SYN_SENT (e.g. the
     264                 :             // spurious pre-connect readiness of a fresh socket) must
     265                 :             // not complete the connect; perform_io() reports EAGAIN
     266                 :             // until a peer is actually established.
     267           46690 :             if (connect_op)
     268                 :             {
     269            8110 :                 auto* cn = connect_op;
     270            8110 :                 if (err)
     271              12 :                     cn->complete(err, 0);
     272                 :                 else
     273            8098 :                     cn->perform_io();
     274                 : 
     275            8110 :                 if (cn->errn == EAGAIN || cn->errn == EWOULDBLOCK)
     276                 :                 {
     277 MIS           0 :                     cn->errn = 0;
     278                 :                 }
     279                 :                 else
     280                 :                 {
     281 HIT        8110 :                     connect_op = nullptr;
     282            8110 :                     local_ops.push(cn);
     283                 :                 }
     284                 :             }
     285           46690 :             if (write_op)
     286                 :             {
     287             200 :                 auto* wr = write_op;
     288             200 :                 if (err)
     289               2 :                     wr->complete(err, 0);
     290                 :                 else
     291             198 :                     wr->perform_io();
     292                 : 
     293             200 :                 if (wr->errn == EAGAIN || wr->errn == EWOULDBLOCK)
     294                 :                 {
     295               1 :                     wr->errn = 0;
     296                 :                 }
     297                 :                 else
     298                 :                 {
     299             199 :                     write_op = nullptr;
     300             199 :                     local_ops.push(wr);
     301                 :                 }
     302                 :             }
     303           46690 :             if (!had_write_op)
     304           38380 :                 write_ready = true;
     305                 : 
     306                 :             // Same re-probe discipline as the wait-for-read dispatch.
     307           46690 :             if (wait_write_op)
     308                 :             {
     309              11 :                 auto* wo = wait_write_op;
     310              11 :                 wo->perform_io();
     311                 : 
     312              11 :                 if (wo->errn == EAGAIN || wo->errn == EWOULDBLOCK)
     313                 :                 {
     314 MIS           0 :                     wo->errn = 0;
     315                 :                 }
     316                 :                 else
     317                 :                 {
     318 HIT          11 :                     wait_write_op = nullptr;
     319              11 :                     local_ops.push(wo);
     320                 :                 }
     321                 :             }
     322                 :         }
     323                 :         // Complete a parked wait-for-error on any error condition.
     324           54216 :         if (ev & reactor_event_error)
     325                 :         {
     326             365 :             if (wait_error_op)
     327                 :             {
     328                 :                 // wait(error) fired on the exceptional condition; name a
     329                 :                 // code even when the kernel exposed none (e.g. urgent
     330                 :                 // data leaves SO_ERROR == 0).
     331               6 :                 int const werr = err ? err : EIO;
     332               6 :                 wait_error_op->complete(werr, 0);
     333               6 :                 local_ops.push(std::exchange(wait_error_op, nullptr));
     334                 :             }
     335                 :         }
     336           54216 :         if (err)
     337                 :         {
     338              23 :             if (read_op)
     339                 :             {
     340 MIS           0 :                 read_op->complete(err, 0);
     341               0 :                 local_ops.push(std::exchange(read_op, nullptr));
     342                 :             }
     343 HIT          23 :             if (write_op)
     344                 :             {
     345 MIS           0 :                 write_op->complete(err, 0);
     346               0 :                 local_ops.push(std::exchange(write_op, nullptr));
     347                 :             }
     348 HIT          23 :             if (connect_op)
     349                 :             {
     350 MIS           0 :                 connect_op->complete(err, 0);
     351               0 :                 local_ops.push(std::exchange(connect_op, nullptr));
     352                 :             }
     353                 :         }
     354 HIT       54216 :     }
     355                 : 
     356                 :     // Execute first handler inline — the scheduler's work_cleanup
     357                 :     // accounts for this as the "consumed" work item. local_ops holds
     358                 :     // only ops, so the popped entry decodes directly.
     359           54216 :     scheduler_op* first = ready_as_op(local_ops.pop());
     360           54216 :     if (first)
     361                 :     {
     362           16057 :         sched->post_deferred_completions(local_ops);
     363           16057 :         (*first)();
     364                 :     }
     365                 :     else
     366                 :     {
     367           38159 :         sched->compensating_work_started();
     368                 :     }
     369           54216 : }
     370                 : 
     371                 : } // namespace boost::corosio::detail
     372                 : 
     373                 : #endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DESCRIPTOR_STATE_HPP
        

Generated by: LCOV version 2.3