LCOV - code coverage report
Current view: top level - corosio/detail - cancel_at_awaitable.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 95.8 % 48 46 2
Test Date: 2026-06-17 18:48:50 Functions: 90.6 % 32 29 3

           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_CANCEL_AT_AWAITABLE_HPP
      11                 : #define BOOST_COROSIO_DETAIL_CANCEL_AT_AWAITABLE_HPP
      12                 : 
      13                 : #include <boost/corosio/detail/timeout_coro.hpp>
      14                 : #include <boost/corosio/detail/except.hpp>
      15                 : #include <boost/capy/ex/io_env.hpp>
      16                 : 
      17                 : #include <chrono>
      18                 : #include <coroutine>
      19                 : #include <new>
      20                 : #include <optional>
      21                 : #include <stdexcept>
      22                 : #include <stop_token>
      23                 : #include <type_traits>
      24                 : #include <utility>
      25                 : 
      26                 : /* Races an inner IoAwaitable against a timer via a shared
      27                 :    stop_source. await_suspend arms the timer by launching a
      28                 :    fire-and-forget timeout_coro, then starts the inner op with
      29                 :    an interposed stop_token. Whichever completes first signals
      30                 :    the stop_source, cancelling the other.
      31                 : 
      32                 :    Parent cancellation is forwarded through a stop_callback
      33                 :    stored in a placement-new buffer (stop_callback is not
      34                 :    movable, but the awaitable must be movable for
      35                 :    transform_awaiter). The buffer is inert during moves
      36                 :    (before await_suspend) and constructed in-place once the
      37                 :    awaitable is pinned on the coroutine frame.
      38                 : 
      39                 :    The timeout_coro can outlive this awaitable — it owns its
      40                 :    env and self-destroys via suspend_never. When Owning is
      41                 :    false the caller-supplied timer must outlive both; when
      42                 :    Owning is true the timer lives in std::optional and is
      43                 :    constructed lazily in await_suspend. */
      44                 : 
      45                 : namespace boost::corosio::detail {
      46                 : 
      47                 : /** Awaitable adapter that cancels an inner operation after a deadline.
      48                 : 
      49                 :     Races the inner awaitable against a timer. A shared stop_source
      50                 :     ties them together: whichever completes first cancels the other.
      51                 :     Parent cancellation is forwarded via stop_callback.
      52                 : 
      53                 :     When @p Owning is `false` (default), the caller supplies a timer
      54                 :     reference that must outlive the awaitable. When @p Owning is
      55                 :     `true`, the timer is constructed internally in `await_suspend`
      56                 :     from the execution context in `io_env`.
      57                 : 
      58                 :     @tparam A The inner IoAwaitable type (decayed).
      59                 :     @tparam Timer The timer type (`timer` or `native_timer<B>`).
      60                 :     @tparam Owning When `true`, the awaitable owns its timer.
      61                 : */
      62                 : template<typename A, typename Timer, bool Owning = false>
      63                 : struct cancel_at_awaitable
      64                 : {
      65                 :     struct stop_forwarder
      66                 :     {
      67                 :         std::stop_source* src_;
      68 HIT           2 :         void operator()() const noexcept
      69                 :         {
      70               2 :             src_->request_stop();
      71               2 :         }
      72                 :     };
      73                 : 
      74                 :     using time_point   = std::chrono::steady_clock::time_point;
      75                 :     using stop_cb_type = std::stop_callback<stop_forwarder>;
      76                 :     using timer_storage =
      77                 :         std::conditional_t<Owning, std::optional<Timer>, Timer*>;
      78                 : 
      79                 :     A inner_;
      80                 :     timer_storage timer_;
      81                 :     time_point deadline_;
      82                 :     std::stop_source stop_src_;
      83                 :     capy::io_env inner_env_;
      84                 :     alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)];
      85                 :     bool cb_active_ = false;
      86                 : 
      87                 :     /// Construct with a caller-supplied timer reference.
      88              18 :     cancel_at_awaitable(A&& inner, Timer& timer, time_point deadline)
      89                 :         requires(!Owning)
      90              18 :         : inner_(std::move(inner))
      91              18 :         , timer_(&timer)
      92              18 :         , deadline_(deadline)
      93                 :     {
      94              18 :     }
      95                 : 
      96                 :     /// Construct without a timer (created in `await_suspend`).
      97              12 :     cancel_at_awaitable(A&& inner, time_point deadline)
      98                 :         requires Owning
      99              12 :         : inner_(std::move(inner))
     100              12 :         , deadline_(deadline)
     101                 :     {
     102              12 :     }
     103                 : 
     104              60 :     ~cancel_at_awaitable()
     105                 :     {
     106              60 :         destroy_parent_cb();
     107              60 :     }
     108                 : 
     109                 :     // Only moved before await_suspend, when cb_active_ is false
     110              30 :     cancel_at_awaitable(cancel_at_awaitable&& o) noexcept(
     111                 :         std::is_nothrow_move_constructible_v<A>)
     112              30 :         : inner_(std::move(o.inner_))
     113              30 :         , timer_(std::move(o.timer_))
     114              30 :         , deadline_(o.deadline_)
     115              30 :         , stop_src_(std::move(o.stop_src_))
     116                 :     {
     117              30 :     }
     118                 : 
     119                 :     cancel_at_awaitable(cancel_at_awaitable const&)            = delete;
     120                 :     cancel_at_awaitable& operator=(cancel_at_awaitable const&) = delete;
     121                 :     cancel_at_awaitable& operator=(cancel_at_awaitable&&)      = delete;
     122                 : 
     123              30 :     bool await_ready() const noexcept
     124                 :     {
     125              30 :         return false;
     126                 :     }
     127                 : 
     128              30 :     auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
     129                 :     {
     130                 :         if constexpr (Owning)
     131                 :         {
     132                 :             // The deadline timer is built here from the awaiting
     133                 :             // coroutine's executor context, the first point at which it
     134                 :             // is known. await_suspend is driven through a noexcept
     135                 :             // wrapper, so a failure cannot be surfaced as a catchable
     136                 :             // exception. An executor whose context is not an io_context
     137                 :             // cannot supply a timer service; silently running the
     138                 :             // operation with no deadline would be a worse failure than
     139                 :             // aborting, so translate the service-lookup error into a
     140                 :             // clear precondition diagnostic. This terminates by design
     141                 :             // (a usage error) rather than dropping the requested timeout.
     142                 :             try
     143                 :             {
     144              12 :                 timer_.emplace(env->executor.context());
     145                 :             }
     146 MIS           0 :             catch (std::logic_error const&)
     147                 :             {
     148               0 :                 throw_logic_error(
     149                 :                     "cancel_after/cancel_at requires an "
     150                 :                     "io_context-backed executor");
     151                 :             }
     152                 :         }
     153                 : 
     154 HIT          30 :         timer_->expires_at(deadline_);
     155                 : 
     156                 :         // Launch fire-and-forget timeout (starts suspended)
     157              30 :         auto timeout = make_timeout(*timer_, stop_src_);
     158              60 :         timeout.h_.promise().set_env_owned(
     159              30 :             {env->executor, stop_src_.get_token(), env->frame_allocator});
     160                 :         // Runs synchronously until timer.wait() suspends
     161              30 :         timeout.h_.resume();
     162                 :         // timeout goes out of scope; destructor is a no-op,
     163                 :         // the coroutine self-destroys via suspend_never
     164                 : 
     165                 :         // Forward parent cancellation
     166              30 :         new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_});
     167              30 :         cb_active_ = true;
     168                 : 
     169                 :         // Start the inner op with our interposed stop_token
     170              30 :         inner_env_ = {
     171              30 :             env->executor, stop_src_.get_token(), env->frame_allocator};
     172              60 :         return inner_.await_suspend(h, &inner_env_);
     173              60 :     }
     174                 : 
     175              30 :     decltype(auto) await_resume()
     176                 :     {
     177                 :         // Cancel whichever is still pending (idempotent)
     178              30 :         stop_src_.request_stop();
     179              30 :         destroy_parent_cb();
     180              30 :         return inner_.await_resume();
     181                 :     }
     182                 : 
     183              90 :     void destroy_parent_cb() noexcept
     184                 :     {
     185              90 :         if (cb_active_)
     186                 :         {
     187              30 :             std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
     188              30 :                 ->~stop_cb_type();
     189              30 :             cb_active_ = false;
     190                 :         }
     191              90 :     }
     192                 : };
     193                 : 
     194                 : } // namespace boost::corosio::detail
     195                 : 
     196                 : #endif
        

Generated by: LCOV version 2.3