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