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