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/capy/error.hpp>
17 : #include <boost/capy/ex/io_env.hpp>
18 : #include <boost/capy/io_result.hpp>
19 :
20 : #include <chrono>
21 : #include <coroutine>
22 : #include <exception>
23 : #include <optional>
24 : #include <stdexcept>
25 :
26 : namespace boost::corosio {
27 :
28 : /** IoAwaitable returned by @ref delay.
29 :
30 : Suspends the calling coroutine until the deadline elapses or
31 : the environment's stop token is activated, whichever comes
32 : first. A deadline already elapsed at suspension, or a stop
33 : token already active, resumes the coroutine inline, without
34 : starting a timer (see Cancellation below). Otherwise the
35 : coroutine resumes through the executor once the timer fires
36 : or a mid-wait cancellation arrives.
37 :
38 : Not intended to be named directly; use the @ref delay factory
39 : overloads instead.
40 :
41 : @par Preconditions
42 : The awaiting coroutine's executor must belong to an
43 : `io_context`. Any other execution context terminates with a
44 : diagnostic, because silently running without a timer would
45 : drop the requested delay.
46 :
47 : @par Cancellation
48 : If stop is already requested before suspension, the coroutine
49 : resumes immediately with `error::canceled`. If stop is
50 : requested while suspended, the pending wait is cancelled and
51 : the coroutine resumes with `error::canceled`. Requesting stop
52 : from another thread while the io_context runs in
53 : single_threaded mode (auto-enabled at concurrency_hint == 1)
54 : is not permitted by io_context's threading rules;
55 : cross-thread cancellation requires a multi-threaded-capable
56 : context.
57 :
58 : @see delay
59 : */
60 : class delay_awaitable
61 : {
62 : // wait() names timer's private awaitable type; decltype is
63 : // the only way to store it here.
64 : using wait_type = decltype(std::declval<detail::timer&>().wait());
65 :
66 : std::chrono::steady_clock::time_point deadline_{};
67 : std::chrono::nanoseconds dur_{};
68 : bool has_deadline_ = false;
69 : bool canceled_ = false;
70 : std::optional<detail::timer> timer_;
71 : std::optional<wait_type> wait_;
72 :
73 : public:
74 : /// Construct an awaitable that waits for `dur` nanoseconds.
75 HIT 10424 : explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
76 10424 : : dur_(dur)
77 : {
78 10424 : }
79 :
80 : /// Construct an awaitable that waits until `tp`.
81 6 : explicit delay_awaitable(
82 : std::chrono::steady_clock::time_point tp) noexcept
83 6 : : deadline_(tp)
84 6 : , has_deadline_(true)
85 : {
86 6 : }
87 :
88 : /// Construct by transferring state from `other`.
89 : // Only moved before await_suspend; wait_ is engaged after.
90 12458 : delay_awaitable(delay_awaitable&&) = default;
91 :
92 : delay_awaitable(delay_awaitable const&) = delete;
93 : delay_awaitable& operator=(delay_awaitable const&) = delete;
94 : delay_awaitable& operator=(delay_awaitable&&) = delete;
95 :
96 : /// Return false unconditionally; see await_suspend.
97 : // The elapsed-deadline fast path must run after the stop-token
98 : // check, and only await_suspend receives the env carrying it.
99 8400 : bool await_ready() const noexcept
100 : {
101 8400 : return false;
102 : }
103 :
104 : /// Resume inline if stopped or elapsed; else wait on a timer.
105 : std::coroutine_handle<>
106 10430 : await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
107 : {
108 10430 : if(env->stop_token.stop_requested())
109 : {
110 4003 : canceled_ = true;
111 4003 : return h;
112 : }
113 :
114 : // Elapsed deadlines complete synchronously, but only once a
115 : // pending stop request has already been ruled out above.
116 12850 : if(has_deadline_ ?
117 6427 : deadline_ <= std::chrono::steady_clock::now() :
118 6423 : dur_.count() <= 0)
119 8 : return h;
120 :
121 : // A non-io_context executor cannot supply a timer service,
122 : // and await_suspend is driven through a noexcept wrapper, so
123 : // translate the service-lookup failure into a clear terminate.
124 : try
125 : {
126 6419 : timer_.emplace(env->executor.context());
127 : }
128 2 : catch(std::logic_error const&)
129 : {
130 2 : detail::throw_logic_error(
131 : "delay requires an io_context-backed executor");
132 2 : }
133 MIS 0 : catch(std::exception const& e)
134 : {
135 0 : detail::throw_logic_error(e.what());
136 0 : }
137 :
138 HIT 6417 : if(has_deadline_)
139 2 : timer_->expires_at(deadline_);
140 : else
141 6415 : timer_->expires_after(dur_);
142 :
143 6417 : wait_.emplace(timer_->wait());
144 6417 : return wait_->await_suspend(h, env);
145 : }
146 :
147 : /// Return empty on expiry, `error::canceled` if stop won.
148 10406 : capy::io_result<> await_resume() noexcept
149 : {
150 10406 : if(canceled_)
151 4003 : return {capy::error::canceled};
152 6403 : if(wait_)
153 6395 : return wait_->await_resume();
154 8 : return {};
155 : }
156 : };
157 :
158 : /** Suspend the current coroutine for a duration.
159 :
160 : Returns an IoAwaitable that completes at or after the
161 : specified duration, or earlier if the environment's stop
162 : token is activated. Zero or negative durations complete
163 : synchronously.
164 :
165 : @par Example
166 : @code
167 : auto [ec] = co_await delay(std::chrono::milliseconds(100));
168 : @endcode
169 :
170 : @param dur The duration to wait.
171 :
172 : @return A @ref delay_awaitable yielding `io_result<>`.
173 : */
174 : template<typename Rep, typename Period>
175 : [[nodiscard]] delay_awaitable
176 10422 : delay(std::chrono::duration<Rep, Period> dur) noexcept
177 : {
178 : using namespace std::chrono;
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>;
182 18792 : auto ns = dsec(dur) >= dsec((nanoseconds::max)())
183 18792 : ? (nanoseconds::max)()
184 10420 : : dsec(dur) <= dsec((nanoseconds::min)())
185 10420 : ? (nanoseconds::min)()
186 10418 : : duration_cast<nanoseconds>(dur);
187 10422 : return delay_awaitable(ns);
188 : }
189 :
190 : /** Suspend the current coroutine until a time point.
191 :
192 : Returns an IoAwaitable that completes at or after `tp`, or
193 : earlier if the environment's stop token is activated. Time
194 : points already reached complete synchronously.
195 :
196 : @param tp The steady-clock time point to wait until.
197 :
198 : @return A @ref delay_awaitable yielding `io_result<>`.
199 : */
200 : [[nodiscard]] inline delay_awaitable
201 6 : delay(std::chrono::steady_clock::time_point tp) noexcept
202 : {
203 6 : return delay_awaitable(tp);
204 : }
205 :
206 : } // namespace boost::corosio
207 :
208 : #endif
|