LCOV - code coverage report
Current view: top level - corosio/detail - timeout_awaitable.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 62 62
Test Date: 2026-07-17 17:14:45 Functions: 89.0 % 73 65 8

           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_DETAIL_TIMEOUT_AWAITABLE_HPP
      11                 : #define BOOST_COROSIO_DETAIL_TIMEOUT_AWAITABLE_HPP
      12                 : 
      13                 : #include <boost/corosio/io_context.hpp>
      14                 : #include <boost/corosio/detail/timeout_coro.hpp>
      15                 : #include <boost/corosio/detail/timer.hpp>
      16                 : #include <boost/corosio/detail/except.hpp>
      17                 : #include <boost/capy/cond.hpp>
      18                 : #include <boost/capy/error.hpp>
      19                 : #include <boost/capy/ex/io_env.hpp>
      20                 : #include <boost/capy/io_result.hpp>
      21                 : 
      22                 : #include <chrono>
      23                 : #include <coroutine>
      24                 : #include <new>
      25                 : #include <optional>
      26                 : #include <stdexcept>
      27                 : #include <stop_token>
      28                 : #include <type_traits>
      29                 : #include <utility>
      30                 : 
      31                 : /* Races an inner IoAwaitable against a timer via a shared
      32                 :    stop_source. await_suspend arms the timer by launching a
      33                 :    fire-and-forget timeout_coro, then starts the inner op with
      34                 :    an interposed stop_token. Whichever completes first signals
      35                 :    the stop_source, cancelling the other.
      36                 : 
      37                 :    Parent cancellation is forwarded through a stop_callback
      38                 :    stored in a placement-new buffer (stop_callback is not
      39                 :    movable, but the awaitable must be movable for
      40                 :    transform_awaiter). The buffer is inert during moves
      41                 :    (before await_suspend) and constructed in-place once the
      42                 :    awaitable is pinned on the coroutine frame.
      43                 : 
      44                 :    The timeout_coro can outlive this awaitable — it owns its
      45                 :    env and self-destroys via suspend_never. The timer lives in
      46                 :    std::optional and is constructed lazily in await_suspend,
      47                 :    once the awaiting coroutine's executor context is known. */
      48                 : 
      49                 : namespace boost::corosio::detail {
      50                 : 
      51                 : // Local stand-in for capy::detail's io_result trait: corosio must not
      52                 : // reach into capy::detail, but the result-mapping switch in
      53                 : // await_resume needs to distinguish io_result from other return types.
      54                 : template<typename T>
      55                 : struct is_io_result : std::false_type
      56                 : {
      57                 : };
      58                 : 
      59                 : template<typename... Ts>
      60                 : struct is_io_result<capy::io_result<Ts...>> : std::true_type
      61                 : {
      62                 : };
      63                 : 
      64                 : template<typename T>
      65                 : inline constexpr bool is_io_result_v = is_io_result<T>::value;
      66                 : 
      67                 : /** Awaitable adapter that cancels an inner operation after a deadline.
      68                 : 
      69                 :     Races the inner awaitable against a timer. A shared stop_source
      70                 :     ties them together: whichever completes first cancels the other.
      71                 :     Parent cancellation is forwarded via stop_callback.
      72                 : 
      73                 :     The timer is constructed internally in `await_suspend` from the
      74                 :     execution context in `io_env`.
      75                 : 
      76                 :     @tparam A The inner IoAwaitable type (decayed).
      77                 : */
      78                 : template<typename A>
      79                 : struct timeout_awaitable
      80                 : {
      81                 :     struct stop_forwarder
      82                 :     {
      83                 :         std::stop_source* src_;
      84 HIT        2004 :         void operator()() const noexcept
      85                 :         {
      86            2004 :             src_->request_stop();
      87            2004 :         }
      88                 :     };
      89                 : 
      90                 :     using time_point   = std::chrono::steady_clock::time_point;
      91                 :     using stop_cb_type = std::stop_callback<stop_forwarder>;
      92                 : 
      93                 :     A inner_;
      94                 :     std::optional<timer> timer_;
      95                 :     time_point deadline_;
      96                 :     std::chrono::nanoseconds dur_{};
      97                 :     bool has_deadline_ = true;
      98                 :     std::stop_source stop_src_;
      99                 :     std::stop_token parent_token_;
     100                 :     capy::io_env inner_env_;
     101                 :     alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)];
     102                 :     bool cb_active_ = false;
     103                 : 
     104                 :     /// Construct without a timer, deadline given as an absolute time.
     105               4 :     timeout_awaitable(A&& inner, time_point deadline)
     106               4 :         : inner_(std::move(inner))
     107               4 :         , deadline_(deadline)
     108                 :     {
     109               4 :     }
     110                 : 
     111                 :     /// Construct without a timer, deadline measured from suspension.
     112            2038 :     timeout_awaitable(A&& inner, std::chrono::nanoseconds dur)
     113            2038 :         : inner_(std::move(inner))
     114            2038 :         , dur_(dur)
     115            2038 :         , has_deadline_(false)
     116                 :     {
     117            2038 :     }
     118                 : 
     119            4084 :     ~timeout_awaitable()
     120                 :     {
     121            4084 :         destroy_parent_cb();
     122            4084 :     }
     123                 : 
     124                 :     // Only moved before await_suspend, when cb_active_ is false
     125            2042 :     timeout_awaitable(timeout_awaitable&& o) noexcept(
     126                 :         std::is_nothrow_move_constructible_v<A>)
     127            2042 :         : inner_(std::move(o.inner_))
     128            2042 :         , timer_(std::move(o.timer_))
     129            2042 :         , deadline_(o.deadline_)
     130            2042 :         , dur_(o.dur_)
     131            2042 :         , has_deadline_(o.has_deadline_)
     132            2042 :         , stop_src_(std::move(o.stop_src_))
     133                 :     {
     134            2042 :     }
     135                 : 
     136                 :     timeout_awaitable(timeout_awaitable const&)            = delete;
     137                 :     timeout_awaitable& operator=(timeout_awaitable const&) = delete;
     138                 :     timeout_awaitable& operator=(timeout_awaitable&&)      = delete;
     139                 : 
     140            2038 :     bool await_ready() const noexcept
     141                 :     {
     142            2038 :         return false;
     143                 :     }
     144                 : 
     145            2042 :     auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
     146                 :     {
     147            2042 :         parent_token_ = env->stop_token;
     148                 : 
     149                 :         // The deadline timer is built here from the awaiting
     150                 :         // coroutine's executor context, the first point at which it
     151                 :         // is known. await_suspend is driven through a noexcept
     152                 :         // wrapper, so a failure cannot be surfaced as a catchable
     153                 :         // exception. An executor whose context is not an io_context
     154                 :         // cannot supply a timer service; silently running the
     155                 :         // operation with no deadline would be a worse failure than
     156                 :         // aborting, so translate the service-lookup error into a
     157                 :         // clear precondition diagnostic. This terminates by design
     158                 :         // (a usage error) rather than dropping the requested timeout.
     159                 :         // The detached timeout coroutine must own its executor by
     160                 :         // value (see timeout_coro::set_env_owned); io_env carries
     161                 :         // only a non-owning executor_ref. Recover the concrete
     162                 :         // executor from the context rather than the executor_ref:
     163                 :         // wrapped executors (a strand over the io_context) satisfy
     164                 :         // the documented precondition but do not expose the io
     165                 :         // executor as their target. The timer construction below
     166                 :         // validates the context is an io_context, and the detached
     167                 :         // coroutine shares only the thread-safe stop_source with
     168                 :         // the caller, so resuming it on the raw io executor instead
     169                 :         // of the caller's wrapper is safe.
     170                 :         try
     171                 :         {
     172            2042 :             timer_.emplace(env->executor.context());
     173                 :         }
     174               4 :         catch (std::logic_error const&)
     175                 :         {
     176               2 :             throw_logic_error(
     177                 :                 "timeout requires an io_context-backed executor");
     178                 :         }
     179                 :         auto ex = static_cast<io_context&>(
     180            2040 :             env->executor.context()).get_executor();
     181                 : 
     182            2040 :         if (has_deadline_)
     183               4 :             timer_->expires_at(deadline_);
     184                 :         else
     185            2036 :             timer_->expires_after(dur_);
     186                 : 
     187                 :         // Launch fire-and-forget timeout (starts suspended)
     188            2040 :         auto timeout = make_timeout(*timer_, stop_src_);
     189            4080 :         timeout.h_.promise().set_env_owned(
     190            2040 :             ex, stop_src_.get_token(), env->frame_allocator);
     191                 :         // Runs synchronously until timer.wait() suspends
     192            2040 :         timeout.h_.resume();
     193                 :         // timeout goes out of scope; destructor is a no-op,
     194                 :         // the coroutine self-destroys via suspend_never
     195                 : 
     196                 :         // Forward parent cancellation
     197            2040 :         new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_});
     198            2040 :         cb_active_ = true;
     199                 : 
     200                 :         // Start the inner op with our interposed stop_token
     201            2040 :         inner_env_ = {
     202            2040 :             env->executor, stop_src_.get_token(), env->frame_allocator};
     203            4080 :         return inner_.await_suspend(h, &inner_env_);
     204            2040 :     }
     205                 : 
     206            2036 :     decltype(auto) await_resume()
     207                 :     {
     208                 :         // Read before request_stop: afterwards stop_requested()
     209                 :         // can no longer distinguish who fired first. This must also
     210                 :         // happen before inner_.await_resume() rather than after: when
     211                 :         // the inner awaitable is itself a timeout_awaitable (nested
     212                 :         // timeout()), our own request_stop() below is visible through
     213                 :         // its parent_token_ (aliasing our stop_src_), and would
     214                 :         // otherwise make its read of "parent" look like a
     215                 :         // cancellation that never happened.
     216            2036 :         bool const parent = parent_token_.stop_requested();
     217            2036 :         bool const fired  = stop_src_.stop_requested();
     218                 : 
     219                 :         // If inner_.await_resume() throws below, request_stop() is
     220                 :         // skipped; the still-armed timeout coroutine is then drained
     221                 :         // by timer_'s destructor rather than by us.
     222            2036 :         auto r = inner_.await_resume();
     223                 : 
     224                 :         // Cancel whichever is still pending (idempotent)
     225            2034 :         stop_src_.request_stop();
     226            2034 :         destroy_parent_cb();
     227                 : 
     228                 :         // Deadline won: stop_src_ is assumed to be the only
     229                 :         // cancellation source, whose only writers are the timer
     230                 :         // coroutine and the parent forwarder, so fired && !parent
     231                 :         // identifies a timeout. A third-party cancellation of the
     232                 :         // inner op (e.g. a socket cancel issued from elsewhere)
     233                 :         // landing in the same window as the deadline firing is
     234                 :         // reported as a timeout.
     235            2054 :         if (fired && !parent &&
     236            2054 :             r.ec == capy::cond::canceled)
     237                 :         {
     238              20 :             std::remove_cvref_t<decltype(r)> t{};
     239              20 :             t.ec = make_error_code(capy::error::timeout);
     240              20 :             return t;
     241                 :         }
     242            2014 :         return r;
     243                 :     }
     244                 : 
     245            6118 :     void destroy_parent_cb() noexcept
     246                 :     {
     247            6118 :         if (cb_active_)
     248                 :         {
     249            2040 :             std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
     250            2040 :                 ->~stop_cb_type();
     251            2040 :             cb_active_ = false;
     252                 :         }
     253            6118 :     }
     254                 : };
     255                 : 
     256                 : } // namespace boost::corosio::detail
     257                 : 
     258                 : #endif
        

Generated by: LCOV version 2.3