TLA Line data Source code
1 : //
2 : // Copyright (c) 2026 Steve Gerbino
3 : //
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)
6 : //
7 : // Official repository: https://github.com/cppalliance/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_DELAY_HPP
11 : #define BOOST_COROSIO_DELAY_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/detail/except.hpp>
15 : #include <boost/corosio/detail/timer.hpp>
16 : #include <boost/corosio/wait_traits.hpp>
17 : #include <boost/capy/error.hpp>
18 : #include <boost/capy/ex/io_env.hpp>
19 : #include <boost/capy/io_result.hpp>
20 :
21 : #include <chrono>
22 : #include <concepts>
23 : #include <coroutine>
24 : #include <exception>
25 : #include <optional>
26 : #include <stdexcept>
27 : #include <system_error>
28 : #include <type_traits>
29 :
30 : namespace boost::corosio {
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
38 HIT 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.
46 2 : if (dur != dur)
47 2 : return nanoseconds::zero();
48 : }
49 23548 : return dsec(dur) >= dsec((nanoseconds::max)())
50 23548 : ? (nanoseconds::max)()
51 25624 : : dsec(dur) <= dsec((nanoseconds::min)())
52 12811 : ? (nanoseconds::min)()
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
60 8802 : emplace_delay_timer(
61 : std::optional<timer>& t, capy::execution_context& ctx)
62 : {
63 : try
64 : {
65 8802 : t.emplace(ctx);
66 : }
67 2 : catch(std::logic_error const&)
68 : {
69 2 : throw_logic_error(
70 : "delay requires an io_context-backed executor");
71 2 : }
72 MIS 0 : catch(std::exception const& e)
73 : {
74 0 : throw_logic_error(e.what());
75 0 : }
76 HIT 8800 : }
77 :
78 : } // namespace detail
79 :
80 : /** IoAwaitable returned by @ref delay.
81 :
82 : Suspends the calling coroutine until the deadline elapses or
83 : the environment's stop token is activated, whichever comes
84 : first. A deadline already elapsed at suspension, or a stop
85 : token already active, resumes the coroutine inline, without
86 : starting a timer (see Cancellation below). Otherwise the
87 : coroutine resumes through the executor once the timer fires
88 : or a mid-wait cancellation arrives.
89 :
90 : Not intended to be named directly; use the @ref delay factory
91 : overloads instead.
92 :
93 : @par Preconditions
94 : The awaiting coroutine's executor must belong to an
95 : `io_context`. Any other execution context terminates with a
96 : diagnostic, because silently running without a timer would
97 : drop the requested delay.
98 :
99 : @par Cancellation
100 : If stop is already requested before suspension, the coroutine
101 : resumes immediately with `error::canceled`. If stop is
102 : requested while suspended, the pending wait is cancelled and
103 : the coroutine resumes with `error::canceled`. Requesting stop
104 : from another thread while the io_context runs in
105 : single_threaded mode (auto-enabled at concurrency_hint == 1)
106 : is not permitted by io_context's threading rules;
107 : cross-thread cancellation requires a multi-threaded-capable
108 : context.
109 :
110 : @see delay
111 : */
112 : class delay_awaitable
113 : {
114 : // wait() names timer's private awaitable type; decltype is
115 : // the only way to store it here.
116 : using wait_type = decltype(std::declval<detail::timer&>().wait());
117 :
118 : std::chrono::steady_clock::time_point deadline_{};
119 : std::chrono::nanoseconds dur_{};
120 : bool has_deadline_ = false;
121 : bool canceled_ = false;
122 : std::optional<detail::timer> timer_;
123 : std::optional<wait_type> wait_;
124 :
125 : public:
126 : /// Construct an awaitable that waits for `dur` nanoseconds.
127 12795 : explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
128 12795 : : dur_(dur)
129 : {
130 12795 : }
131 :
132 : /// Construct an awaitable that waits until `tp`.
133 16 : explicit delay_awaitable(
134 : std::chrono::steady_clock::time_point tp) noexcept
135 16 : : deadline_(tp)
136 16 : , has_deadline_(true)
137 : {
138 16 : }
139 :
140 : /// Construct by transferring state from `other`.
141 : // Only moved before await_suspend; wait_ is engaged after.
142 14839 : delay_awaitable(delay_awaitable&&) = default;
143 :
144 : delay_awaitable(delay_awaitable const&) = delete;
145 : delay_awaitable& operator=(delay_awaitable const&) = delete;
146 : delay_awaitable& operator=(delay_awaitable&&) = delete;
147 :
148 : /// Return false unconditionally; see await_suspend.
149 : // The elapsed-deadline fast path must run after the stop-token
150 : // check, and only await_suspend receives the env carrying it.
151 12809 : bool await_ready() const noexcept
152 : {
153 12809 : return false;
154 : }
155 :
156 : /// Resume inline if stopped or elapsed; else wait on a timer.
157 : std::coroutine_handle<>
158 12811 : await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
159 : {
160 12811 : if(env->stop_token.stop_requested())
161 : {
162 4011 : canceled_ = true;
163 4011 : return h;
164 : }
165 :
166 : // Elapsed deadlines complete synchronously, but only once a
167 : // pending stop request has already been ruled out above.
168 17586 : if(has_deadline_ ?
169 8800 : deadline_ <= std::chrono::steady_clock::now() :
170 8786 : dur_.count() <= 0)
171 8 : return h;
172 :
173 8792 : detail::emplace_delay_timer(timer_, env->executor.context());
174 :
175 8790 : if(has_deadline_)
176 12 : timer_->expires_at(deadline_);
177 : else
178 8778 : timer_->expires_after(dur_);
179 :
180 8790 : wait_.emplace(timer_->wait());
181 8790 : return wait_->await_suspend(h, env);
182 : }
183 :
184 : /// Return empty on expiry, `error::canceled` if stop won.
185 12785 : capy::io_result<> await_resume() noexcept
186 : {
187 12785 : if(canceled_)
188 4011 : return {capy::error::canceled};
189 8774 : if(wait_)
190 8766 : return wait_->await_resume();
191 8 : return {};
192 : }
193 : };
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
230 22 : next_wait(typename Clock::time_point now) const noexcept
231 : {
232 22 : return detail::clamp_to_ns(
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.
239 20 : static bool on_fire(void* ctx) noexcept
240 : {
241 20 : auto* self = static_cast<clock_delay_awaitable*>(ctx);
242 : // Canceled: resume and surface the error
243 20 : if(self->w_.ec_)
244 2 : return false;
245 18 : auto now = Clock::now();
246 18 : if(now >= self->deadline_)
247 6 : return false;
248 : // Re-publish and return without touching the node again:
249 : // the wait may complete on another thread immediately after.
250 12 : if(self->timer_->rearm_wait(self->w_, self->next_wait(now)))
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.
254 MIS 0 : self->w_.ec_ = std::make_error_code(std::errc::not_enough_memory);
255 0 : return false;
256 : }
257 :
258 : public:
259 : /// Construct an awaitable that waits until `tp` on `Clock`.
260 HIT 1016 : explicit clock_delay_awaitable(
261 : typename Clock::time_point tp) noexcept
262 1016 : : deadline_(tp)
263 : {
264 1016 : }
265 :
266 : /// Construct by transferring the deadline from `other`.
267 : // Only moved before await_suspend; w_ is quiescent until then.
268 1016 : clock_delay_awaitable(clock_delay_awaitable&& other) noexcept
269 1016 : : deadline_(other.deadline_)
270 : {
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.
282 1016 : bool await_ready() const noexcept
283 : {
284 1016 : return false;
285 : }
286 :
287 : /// Resume inline if stopped or reached; else wait on a timer.
288 : std::coroutine_handle<>
289 1016 : await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
290 : {
291 1016 : if(env->stop_token.stop_requested())
292 : {
293 1004 : canceled_ = true;
294 1004 : return h;
295 : }
296 :
297 12 : auto now = Clock::now();
298 12 : if(now >= deadline_)
299 2 : return h;
300 :
301 10 : detail::emplace_delay_timer(timer_, env->executor.context());
302 :
303 10 : timer_->expires_after(next_wait(now));
304 :
305 10 : w_.bind(h, *env);
306 10 : w_.on_fire_ = &on_fire;
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.
311 10 : return timer_->publish_wait(w_);
312 : }
313 :
314 : /// Return empty on deadline, `error::canceled` if stop won.
315 1014 : capy::io_result<> await_resume() noexcept
316 : {
317 1014 : if(canceled_)
318 1004 : return {capy::error::canceled};
319 10 : if(timer_)
320 8 : return {w_.ec_};
321 2 : return {};
322 : }
323 : };
324 :
325 : /** Suspend the current coroutine for a duration.
326 :
327 : Returns an IoAwaitable that completes at or after the
328 : specified duration, or earlier if the environment's stop
329 : token is activated. Zero or negative durations complete
330 : synchronously.
331 :
332 : @par Example
333 : @code
334 : auto [ec] = co_await delay(std::chrono::milliseconds(100));
335 : @endcode
336 :
337 : @param dur The duration to wait.
338 :
339 : @return A @ref delay_awaitable yielding `io_result<>`.
340 : */
341 : template<typename Rep, typename Period>
342 : [[nodiscard]] delay_awaitable
343 12793 : delay(std::chrono::duration<Rep, Period> dur) noexcept
344 : {
345 12793 : return delay_awaitable(detail::clamp_to_ns(dur));
346 : }
347 :
348 : /** Suspend the current coroutine until a time point.
349 :
350 : Returns an IoAwaitable that completes at or after `tp`, or
351 : earlier if the environment's stop token is activated. Time
352 : points already reached complete synchronously.
353 :
354 : @param tp The steady-clock time point to wait until.
355 :
356 : @return A @ref delay_awaitable yielding `io_result<>`.
357 : */
358 : [[nodiscard]] inline delay_awaitable
359 16 : delay(std::chrono::steady_clock::time_point tp) noexcept
360 : {
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
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>(
403 1016 : std::chrono::ceil<typename Clock::duration>(tp));
404 : }
405 :
406 : } // namespace boost::corosio
407 :
408 : #endif
|