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