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