94.68% Lines (89/94) 100.00% Functions (18/18)
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_DELAY_HPP 10   #ifndef BOOST_COROSIO_DELAY_HPP
11   #define BOOST_COROSIO_DELAY_HPP 11   #define BOOST_COROSIO_DELAY_HPP
12   12  
13   #include <boost/corosio/detail/config.hpp> 13   #include <boost/corosio/detail/config.hpp>
14   #include <boost/corosio/detail/except.hpp> 14   #include <boost/corosio/detail/except.hpp>
15   #include <boost/corosio/detail/timer.hpp> 15   #include <boost/corosio/detail/timer.hpp>
  16 + #include <boost/corosio/wait_traits.hpp>
16   #include <boost/capy/error.hpp> 17   #include <boost/capy/error.hpp>
17   #include <boost/capy/ex/io_env.hpp> 18   #include <boost/capy/ex/io_env.hpp>
18   #include <boost/capy/io_result.hpp> 19   #include <boost/capy/io_result.hpp>
19   20  
20   #include <chrono> 21   #include <chrono>
  22 + #include <concepts>
21   #include <coroutine> 23   #include <coroutine>
22   #include <exception> 24   #include <exception>
23   #include <optional> 25   #include <optional>
24   #include <stdexcept> 26   #include <stdexcept>
  27 + #include <system_error>
  28 + #include <type_traits>
25   29  
26   namespace boost::corosio { 30   namespace boost::corosio {
27   31  
  32 + namespace detail {
  33 +
  34 + // Narrow reps wrap if nanoseconds::max() is converted into them;
  35 + // a double comparison clamps safely in both directions.
  36 + template<typename Rep, typename Period>
  37 + std::chrono::nanoseconds
HITGNC   38 + 12815 clamp_to_ns(std::chrono::duration<Rep, Period> dur) noexcept
  39 + {
  40 + using namespace std::chrono;
  41 + using dsec = duration<double>;
  42 + if constexpr (std::is_floating_point_v<Rep>)
  43 + {
  44 + // NaN fails both clamp comparisons and would reach the
  45 + // cast; treat it as no wait rather than undefined behavior.
HITGNC   46 + 2 if (dur != dur)
HITGNC   47 + 2 return nanoseconds::zero();
  48 + }
HITGNC   49 + 23548 return dsec(dur) >= dsec((nanoseconds::max)())
HITGNC   50 + 23548 ? (nanoseconds::max)()
HITGNC   51 + 25624 : dsec(dur) <= dsec((nanoseconds::min)())
HITGNC   52 + 12811 ? (nanoseconds::min)()
HITGNC   53 + 12813 : duration_cast<nanoseconds>(dur);
  54 + }
  55 +
  56 + // A non-io_context executor cannot supply a timer service, and
  57 + // await_suspend is driven through a noexcept wrapper, so translate
  58 + // the service-lookup failure into a clear terminate.
  59 + inline void
HITGNC   60 + 8802 emplace_delay_timer(
  61 + std::optional<timer>& t, capy::execution_context& ctx)
  62 + {
  63 + try
  64 + {
HITGNC   65 + 8802 t.emplace(ctx);
  66 + }
HITGNC   67 + 2 catch(std::logic_error const&)
  68 + {
HITGNC   69 + 2 throw_logic_error(
  70 + "delay requires an io_context-backed executor");
HITGNC   71 + 2 }
MISUNC   72 + catch(std::exception const& e)
  73 + {
MISUNC   74 + throw_logic_error(e.what());
MISUNC   75 + }
HITGNC   76 + 8800 }
  77 +
  78 + } // namespace detail
  79 +
28   /** IoAwaitable returned by @ref delay. 80   /** IoAwaitable returned by @ref delay.
29   81  
30   Suspends the calling coroutine until the deadline elapses or 82   Suspends the calling coroutine until the deadline elapses or
31   the environment's stop token is activated, whichever comes 83   the environment's stop token is activated, whichever comes
32   first. A deadline already elapsed at suspension, or a stop 84   first. A deadline already elapsed at suspension, or a stop
33   token already active, resumes the coroutine inline, without 85   token already active, resumes the coroutine inline, without
34   starting a timer (see Cancellation below). Otherwise the 86   starting a timer (see Cancellation below). Otherwise the
35   coroutine resumes through the executor once the timer fires 87   coroutine resumes through the executor once the timer fires
36   or a mid-wait cancellation arrives. 88   or a mid-wait cancellation arrives.
37   89  
38   Not intended to be named directly; use the @ref delay factory 90   Not intended to be named directly; use the @ref delay factory
39   overloads instead. 91   overloads instead.
40   92  
41   @par Preconditions 93   @par Preconditions
42   The awaiting coroutine's executor must belong to an 94   The awaiting coroutine's executor must belong to an
43   `io_context`. Any other execution context terminates with a 95   `io_context`. Any other execution context terminates with a
44   diagnostic, because silently running without a timer would 96   diagnostic, because silently running without a timer would
45   drop the requested delay. 97   drop the requested delay.
46   98  
47   @par Cancellation 99   @par Cancellation
48   If stop is already requested before suspension, the coroutine 100   If stop is already requested before suspension, the coroutine
49   resumes immediately with `error::canceled`. If stop is 101   resumes immediately with `error::canceled`. If stop is
50   requested while suspended, the pending wait is cancelled and 102   requested while suspended, the pending wait is cancelled and
51   the coroutine resumes with `error::canceled`. Requesting stop 103   the coroutine resumes with `error::canceled`. Requesting stop
52   from another thread while the io_context runs in 104   from another thread while the io_context runs in
53   single_threaded mode (auto-enabled at concurrency_hint == 1) 105   single_threaded mode (auto-enabled at concurrency_hint == 1)
54   is not permitted by io_context's threading rules; 106   is not permitted by io_context's threading rules;
55   cross-thread cancellation requires a multi-threaded-capable 107   cross-thread cancellation requires a multi-threaded-capable
56   context. 108   context.
57   109  
58   @see delay 110   @see delay
59   */ 111   */
60   class delay_awaitable 112   class delay_awaitable
61   { 113   {
62   // wait() names timer's private awaitable type; decltype is 114   // wait() names timer's private awaitable type; decltype is
63   // the only way to store it here. 115   // the only way to store it here.
64   using wait_type = decltype(std::declval<detail::timer&>().wait()); 116   using wait_type = decltype(std::declval<detail::timer&>().wait());
65   117  
66   std::chrono::steady_clock::time_point deadline_{}; 118   std::chrono::steady_clock::time_point deadline_{};
67   std::chrono::nanoseconds dur_{}; 119   std::chrono::nanoseconds dur_{};
68   bool has_deadline_ = false; 120   bool has_deadline_ = false;
69   bool canceled_ = false; 121   bool canceled_ = false;
70   std::optional<detail::timer> timer_; 122   std::optional<detail::timer> timer_;
71   std::optional<wait_type> wait_; 123   std::optional<wait_type> wait_;
72   124  
73   public: 125   public:
74   /// Construct an awaitable that waits for `dur` nanoseconds. 126   /// Construct an awaitable that waits for `dur` nanoseconds.
HITCBC 75   8861 explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept 127   12795 explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
HITCBC 76   8861 : dur_(dur) 128   12795 : dur_(dur)
77   { 129   {
HITCBC 78   8861 } 130   12795 }
79   131  
80   /// Construct an awaitable that waits until `tp`. 132   /// Construct an awaitable that waits until `tp`.
HITCBC 81   16 explicit delay_awaitable( 133   16 explicit delay_awaitable(
82   std::chrono::steady_clock::time_point tp) noexcept 134   std::chrono::steady_clock::time_point tp) noexcept
HITCBC 83   16 : deadline_(tp) 135   16 : deadline_(tp)
HITCBC 84   16 , has_deadline_(true) 136   16 , has_deadline_(true)
85   { 137   {
HITCBC 86   16 } 138   16 }
87   139  
88   /// Construct by transferring state from `other`. 140   /// Construct by transferring state from `other`.
89   // Only moved before await_suspend; wait_ is engaged after. 141   // Only moved before await_suspend; wait_ is engaged after.
HITCBC 90   10905 delay_awaitable(delay_awaitable&&) = default; 142   14839 delay_awaitable(delay_awaitable&&) = default;
91   143  
92   delay_awaitable(delay_awaitable const&) = delete; 144   delay_awaitable(delay_awaitable const&) = delete;
93   delay_awaitable& operator=(delay_awaitable const&) = delete; 145   delay_awaitable& operator=(delay_awaitable const&) = delete;
94   delay_awaitable& operator=(delay_awaitable&&) = delete; 146   delay_awaitable& operator=(delay_awaitable&&) = delete;
95   147  
96   /// Return false unconditionally; see await_suspend. 148   /// Return false unconditionally; see await_suspend.
97   // The elapsed-deadline fast path must run after the stop-token 149   // The elapsed-deadline fast path must run after the stop-token
98   // check, and only await_suspend receives the env carrying it. 150   // check, and only await_suspend receives the env carrying it.
HITCBC 99   8875 bool await_ready() const noexcept 151   12809 bool await_ready() const noexcept
100   { 152   {
HITCBC 101   8875 return false; 153   12809 return false;
102   } 154   }
103   155  
104   /// Resume inline if stopped or elapsed; else wait on a timer. 156   /// Resume inline if stopped or elapsed; else wait on a timer.
105   std::coroutine_handle<> 157   std::coroutine_handle<>
HITCBC 106   8877 await_suspend(std::coroutine_handle<> h, capy::io_env const* env) 158   12811 await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
107   { 159   {
HITCBC 108   8877 if(env->stop_token.stop_requested()) 160   12811 if(env->stop_token.stop_requested())
109   { 161   {
HITCBC 110   4006 canceled_ = true; 162   4011 canceled_ = true;
HITCBC 111   4006 return h; 163   4011 return h;
112   } 164   }
113   165  
114   // Elapsed deadlines complete synchronously, but only once a 166   // Elapsed deadlines complete synchronously, but only once a
115   // pending stop request has already been ruled out above. 167   // pending stop request has already been ruled out above.
HITCBC 116   9728 if(has_deadline_ ? 168   17586 if(has_deadline_ ?
HITCBC 117   4871 deadline_ <= std::chrono::steady_clock::now() : 169   8800 deadline_ <= std::chrono::steady_clock::now() :
HITCBC 118   4857 dur_.count() <= 0) 170   8786 dur_.count() <= 0)
HITCBC 119   8 return h; 171   8 return h;
120   172  
HITGIC 121 - // A non-io_context executor cannot supply a timer service, 173 + 8792 detail::emplace_delay_timer(timer_, env->executor.context());
122 - // and await_suspend is driven through a noexcept wrapper, so  
123 - // translate the service-lookup failure into a clear terminate.  
124 - try  
125 - {  
DCB 126 - 4863 timer_.emplace(env->executor.context());  
127 - }  
DCB 128 - 2 catch(std::logic_error const&)  
129 - {  
DCB 130 - 2 detail::throw_logic_error(  
131 - "delay requires an io_context-backed executor");  
DCB 132 - 2 }  
DUB 133 - catch(std::exception const& e)  
134 - {  
DUB 135 - detail::throw_logic_error(e.what());  
DUB 136 - }  
137   174  
HITCBC 138   4861 if(has_deadline_) 175   8790 if(has_deadline_)
HITCBC 139   12 timer_->expires_at(deadline_); 176   12 timer_->expires_at(deadline_);
140   else 177   else
HITCBC 141   4849 timer_->expires_after(dur_); 178   8778 timer_->expires_after(dur_);
142   179  
HITCBC 143   4861 wait_.emplace(timer_->wait()); 180   8790 wait_.emplace(timer_->wait());
HITCBC 144   4861 return wait_->await_suspend(h, env); 181   8790 return wait_->await_suspend(h, env);
145   } 182   }
146   183  
147   /// Return empty on expiry, `error::canceled` if stop won. 184   /// Return empty on expiry, `error::canceled` if stop won.
HITCBC 148   8851 capy::io_result<> await_resume() noexcept 185   12785 capy::io_result<> await_resume() noexcept
149   { 186   {
HITCBC 150   8851 if(canceled_) 187   12785 if(canceled_)
HITCBC 151   4006 return {capy::error::canceled}; 188   4011 return {capy::error::canceled};
HITCBC 152   4845 if(wait_) 189   8774 if(wait_)
HITCBC 153   4837 return wait_->await_resume(); 190   8766 return wait_->await_resume();
HITCBC 154   8 return {}; 191   8 return {};
155   } 192   }
156   }; 193   };
157   194  
  195 + /** IoAwaitable returned by the clock overloads of @ref delay.
  196 +
  197 + Suspends the calling coroutine until `Clock::now()` reaches the
  198 + deadline or the environment's stop token is activated. The wait
  199 + is a sequence of steady-clock timer waits: after each expiry the
  200 + clock is re-read and, if the deadline is unreached, the same
  201 + frame-embedded waiter is re-published for the next
  202 + `Traits::to_wait_duration` cap — without resuming the coroutine
  203 + and without allocating.
  204 +
  205 + Not intended to be named directly; use the @ref delay factory
  206 + overloads instead.
  207 +
  208 + @par Preconditions
  209 + The awaiting coroutine's executor must belong to an
  210 + `io_context`. Any other execution context terminates with a
  211 + diagnostic, because silently running without a timer would
  212 + drop the requested delay.
  213 +
  214 + @par Cancellation
  215 + Identical to @ref delay_awaitable: stop already requested
  216 + resumes inline with `error::canceled`; stop while suspended
  217 + cancels the pending wait, including between re-arms.
  218 +
  219 + @see delay, wait_traits
  220 + */
  221 + template<class Clock, class Traits>
  222 + class clock_delay_awaitable
  223 + {
  224 + typename Clock::time_point deadline_{};
  225 + bool canceled_ = false;
  226 + std::optional<detail::timer> timer_;
  227 + detail::waiter_node w_;
  228 +
  229 + std::chrono::nanoseconds
HITGNC   230 + 22 next_wait(typename Clock::time_point now) const noexcept
  231 + {
HITGNC   232 + 22 return detail::clamp_to_ns(
HITGNC   233 + 44 Traits::to_wait_duration(deadline_ - now));
  234 + }
  235 +
  236 + // Runs on the scheduler thread executing the completion op,
  237 + // before the continuation is posted, so the frame cannot die
  238 + // concurrently.
HITGNC   239 + 20 static bool on_fire(void* ctx) noexcept
  240 + {
HITGNC   241 + 20 auto* self = static_cast<clock_delay_awaitable*>(ctx);
  242 + // Canceled: resume and surface the error
HITGNC   243 + 20 if(self->w_.ec_)
HITGNC   244 + 2 return false;
HITGNC   245 + 18 auto now = Clock::now();
HITGNC   246 + 18 if(now >= self->deadline_)
HITGNC   247 + 6 return false;
  248 + // Re-publish and return without touching the node again:
  249 + // the wait may complete on another thread immediately after.
HITGNC   250 + 12 if(self->timer_->rearm_wait(self->w_, self->next_wait(now)))
HITGNC   251 + 12 return true;
  252 + // Heap growth failed; finish the wait with an error rather
  253 + // than strand the frame with an unbalanced work count.
MISUNC   254 + self->w_.ec_ = std::make_error_code(std::errc::not_enough_memory);
MISUNC   255 + return false;
  256 + }
  257 +
  258 + public:
  259 + /// Construct an awaitable that waits until `tp` on `Clock`.
HITGNC   260 + 1016 explicit clock_delay_awaitable(
  261 + typename Clock::time_point tp) noexcept
HITGNC   262 + 1016 : deadline_(tp)
  263 + {
HITGNC   264 + 1016 }
  265 +
  266 + /// Construct by transferring the deadline from `other`.
  267 + // Only moved before await_suspend; w_ is quiescent until then.
HITGNC   268 + 1016 clock_delay_awaitable(clock_delay_awaitable&& other) noexcept
HITGNC   269 + 1016 : deadline_(other.deadline_)
  270 + {
HITGNC   271 + 1016 }
  272 +
  273 + clock_delay_awaitable(clock_delay_awaitable const&) = delete;
  274 + clock_delay_awaitable&
  275 + operator=(clock_delay_awaitable const&) = delete;
  276 + clock_delay_awaitable&
  277 + operator=(clock_delay_awaitable&&) = delete;
  278 +
  279 + /// Return false unconditionally; see await_suspend.
  280 + // The elapsed-deadline fast path must run after the stop-token
  281 + // check, and only await_suspend receives the env carrying it.
HITGNC   282 + 1016 bool await_ready() const noexcept
  283 + {
HITGNC   284 + 1016 return false;
  285 + }
  286 +
  287 + /// Resume inline if stopped or reached; else wait on a timer.
  288 + std::coroutine_handle<>
HITGNC   289 + 1016 await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
  290 + {
HITGNC   291 + 1016 if(env->stop_token.stop_requested())
  292 + {
HITGNC   293 + 1004 canceled_ = true;
HITGNC   294 + 1004 return h;
  295 + }
  296 +
HITGNC   297 + 12 auto now = Clock::now();
HITGNC   298 + 12 if(now >= deadline_)
HITGNC   299 + 2 return h;
  300 +
HITGNC   301 + 10 detail::emplace_delay_timer(timer_, env->executor.context());
  302 +
HITGNC   303 + 10 timer_->expires_after(next_wait(now));
  304 +
HITGNC   305 + 10 w_.bind(h, *env);
HITGNC   306 + 10 w_.on_fire_ = &on_fire;
HITGNC   307 + 10 w_.on_fire_ctx_ = this;
  308 + // Never the elapsed fast path: a capped expiry that elapses
  309 + // before publication must still reach on_fire, not complete
  310 + // the clock wait early.
HITGNC   311 + 10 return timer_->publish_wait(w_);
  312 + }
  313 +
  314 + /// Return empty on deadline, `error::canceled` if stop won.
HITGNC   315 + 1014 capy::io_result<> await_resume() noexcept
  316 + {
HITGNC   317 + 1014 if(canceled_)
HITGNC   318 + 1004 return {capy::error::canceled};
HITGNC   319 + 10 if(timer_)
HITGNC   320 + 8 return {w_.ec_};
HITGNC   321 + 2 return {};
  322 + }
  323 + };
  324 +
158   /** Suspend the current coroutine for a duration. 325   /** Suspend the current coroutine for a duration.
159   326  
160   Returns an IoAwaitable that completes at or after the 327   Returns an IoAwaitable that completes at or after the
161   specified duration, or earlier if the environment's stop 328   specified duration, or earlier if the environment's stop
162   token is activated. Zero or negative durations complete 329   token is activated. Zero or negative durations complete
163   synchronously. 330   synchronously.
164   331  
165   @par Example 332   @par Example
166   @code 333   @code
167   auto [ec] = co_await delay(std::chrono::milliseconds(100)); 334   auto [ec] = co_await delay(std::chrono::milliseconds(100));
168   @endcode 335   @endcode
169   336  
170   @param dur The duration to wait. 337   @param dur The duration to wait.
171   338  
172   @return A @ref delay_awaitable yielding `io_result<>`. 339   @return A @ref delay_awaitable yielding `io_result<>`.
173   */ 340   */
174   template<typename Rep, typename Period> 341   template<typename Rep, typename Period>
175   [[nodiscard]] delay_awaitable 342   [[nodiscard]] delay_awaitable
HITCBC 176   8859 delay(std::chrono::duration<Rep, Period> dur) noexcept 343   12793 delay(std::chrono::duration<Rep, Period> dur) noexcept
177   { 344   {
HITGIC 178 - using namespace std::chrono; 345 + 12793 return delay_awaitable(detail::clamp_to_ns(dur));
179 - // Narrow reps wrap if nanoseconds::max() is converted into them;  
180 - // a double comparison clamps safely in both directions.  
181 - using dsec = duration<double>;  
DCB 182 - 15662 auto ns = dsec(dur) >= dsec((nanoseconds::max)())  
DCB 183 - 15662 ? (nanoseconds::max)()  
DCB 184 - 8857 : dsec(dur) <= dsec((nanoseconds::min)())  
DCB 185 - 8857 ? (nanoseconds::min)()  
DCB 186 - 8855 : duration_cast<nanoseconds>(dur);  
DCB 187 - 8859 return delay_awaitable(ns);  
188   } 346   }
189   347  
190   /** Suspend the current coroutine until a time point. 348   /** Suspend the current coroutine until a time point.
191   349  
192   Returns an IoAwaitable that completes at or after `tp`, or 350   Returns an IoAwaitable that completes at or after `tp`, or
193   earlier if the environment's stop token is activated. Time 351   earlier if the environment's stop token is activated. Time
194   points already reached complete synchronously. 352   points already reached complete synchronously.
195   353  
196   @param tp The steady-clock time point to wait until. 354   @param tp The steady-clock time point to wait until.
197   355  
198   @return A @ref delay_awaitable yielding `io_result<>`. 356   @return A @ref delay_awaitable yielding `io_result<>`.
199   */ 357   */
200   [[nodiscard]] inline delay_awaitable 358   [[nodiscard]] inline delay_awaitable
HITCBC 201   16 delay(std::chrono::steady_clock::time_point tp) noexcept 359   16 delay(std::chrono::steady_clock::time_point tp) noexcept
202   { 360   {
HITCBC 203   16 return delay_awaitable(tp); 361   16 return delay_awaitable(tp);
  362 + }
  363 +
  364 + /** Suspend the current coroutine until a time point on `Clock`.
  365 +
  366 + Returns an IoAwaitable that completes at or after the first
  367 + observation of `Clock::now() >= tp`, or earlier if the
  368 + environment's stop token is activated. The wait is one or more
  369 + bounded steady-clock waits, re-reading `Clock::now()` after
  370 + each; `Traits::to_wait_duration` bounds each one. With the
  371 + default @ref wait_traits a single full-length wait is used, so
  372 + an adjustment of `Clock` mid-wait is observed only at natural
  373 + wakeup; supply capping traits to bound that latency. Time
  374 + points already reached complete synchronously.
  375 +
  376 + @note `Clock::now()` and `Traits::to_wait_duration` are invoked
  377 + on the io_context's run thread and must not throw or block.
  378 +
  379 + @par Example
  380 + @code
  381 + auto [ec] = co_await delay(
  382 + std::chrono::system_clock::now() + std::chrono::minutes(5));
  383 + @endcode
  384 +
  385 + @tparam Traits The wait-traits policy; `void` selects
  386 + @ref wait_traits.
  387 +
  388 + @param tp The time point to wait until.
  389 +
  390 + @return A @ref clock_delay_awaitable yielding `io_result<>`.
  391 + */
  392 + template<class Traits = void, class Clock, class Duration>
  393 + requires (!std::same_as<Clock, std::chrono::steady_clock>) &&
  394 + (std::is_void_v<Traits> || WaitTraits<Traits, Clock>)
  395 + [[nodiscard]] auto
HITGNC   396 + 1016 delay(std::chrono::time_point<Clock, Duration> tp) noexcept
  397 + {
  398 + using traits_type = std::conditional_t<
  399 + std::is_void_v<Traits>, wait_traits<Clock>, Traits>;
  400 + // ceil preserves completes-at-or-after when Duration is coarser
  401 + // than the clock's native duration
  402 + return clock_delay_awaitable<Clock, traits_type>(
HITGNC   403 + 1016 std::chrono::ceil<typename Clock::duration>(tp));
204   } 404   }
205   405  
206   } // namespace boost::corosio 406   } // namespace boost::corosio
207   407  
208   #endif 408   #endif