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