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