include/boost/capy/delay.hpp

98.1% Lines (52/53) 100.0% List of functions (9/9) 81.2% Branches (13/16)
f(x) Functions (9)
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Michael Vandeberg
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/capy
8 //
9
10 #ifndef BOOST_CAPY_DELAY_HPP
11 #define BOOST_CAPY_DELAY_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/ex/executor_ref.hpp>
15 #include <boost/capy/ex/io_env.hpp>
16 #include <boost/capy/ex/detail/timer_service.hpp>
17
18 #include <atomic>
19 #include <chrono>
20 #include <coroutine>
21 #include <new>
22 #include <stop_token>
23 #include <utility>
24
25 namespace boost {
26 namespace capy {
27
28 /** IoAwaitable returned by @ref delay.
29
30 Suspends the calling coroutine until the deadline elapses
31 or the environment's stop token is activated, whichever
32 comes first. Resumption is always posted through the
33 executor, never inline on the timer thread.
34
35 Not intended to be named directly; use the @ref delay
36 factory function instead.
37
38 @par Cancellation
39
40 If `stop_requested()` is true before suspension, the
41 coroutine resumes immediately without scheduling a timer.
42 If stop is requested while suspended, the stop callback
43 claims the resume and posts it through the executor; the
44 pending timer is cancelled on the next `await_resume` or
45 destructor call.
46
47 @par Thread Safety
48
49 A single `delay_awaitable` must not be awaited concurrently.
50 Multiple independent `delay()` calls on the same
51 execution_context are safe and share one timer thread.
52
53 @see delay, timeout
54 */
55 class delay_awaitable
56 {
57 std::chrono::nanoseconds dur_;
58
59 detail::timer_service* ts_ = nullptr;
60 detail::timer_service::timer_id tid_ = 0;
61
62 // Declared before stop_cb_buf_: the callback
63 // accesses these members, so they must still be
64 // alive if the stop_cb_ destructor blocks.
65 std::atomic<bool> claimed_{false};
66 bool canceled_ = false;
67 bool stop_cb_active_ = false;
68
69 struct cancel_fn
70 {
71 delay_awaitable* self_;
72 executor_ref ex_;
73 std::coroutine_handle<> h_;
74
75 1x void operator()() const noexcept
76 {
77
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1x if(!self_->claimed_.exchange(
78 true, std::memory_order_acq_rel))
79 {
80 1x self_->canceled_ = true;
81 1x ex_.post(h_);
82 }
83 1x }
84 };
85
86 using stop_cb_t = std::stop_callback<cancel_fn>;
87
88 // Aligned storage for the stop callback.
89 // Declared last: its destructor may block while
90 // the callback accesses the members above.
91 #ifdef _MSC_VER
92 # pragma warning(push)
93 # pragma warning(disable: 4324)
94 #endif
95 alignas(stop_cb_t)
96 unsigned char stop_cb_buf_[sizeof(stop_cb_t)];
97 #ifdef _MSC_VER
98 # pragma warning(pop)
99 #endif
100
101 19x stop_cb_t& stop_cb_() noexcept
102 {
103 19x return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_);
104 }
105
106 public:
107 27x explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
108 27x : dur_(dur)
109 {
110 27x }
111
112 /// @pre The stop callback must not be active
113 /// (i.e. the object has not yet been awaited).
114 58x delay_awaitable(delay_awaitable&& o) noexcept
115 58x : dur_(o.dur_)
116 58x , ts_(o.ts_)
117 58x , tid_(o.tid_)
118 58x , claimed_(o.claimed_.load(std::memory_order_relaxed))
119 58x , canceled_(o.canceled_)
120 58x , stop_cb_active_(std::exchange(o.stop_cb_active_, false))
121 {
122 58x }
123
124 85x ~delay_awaitable()
125 {
126
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 84 times.
85x if(stop_cb_active_)
127 1x stop_cb_().~stop_cb_t();
128
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 66 times.
85x if(ts_)
129 19x ts_->cancel(tid_);
130 85x }
131
132 delay_awaitable(delay_awaitable const&) = delete;
133 delay_awaitable& operator=(delay_awaitable const&) = delete;
134 delay_awaitable& operator=(delay_awaitable&&) = delete;
135
136 26x bool await_ready() const noexcept
137 {
138 26x return dur_.count() <= 0;
139 }
140
141 std::coroutine_handle<>
142 24x await_suspend(
143 std::coroutine_handle<> h,
144 io_env const* env) noexcept
145 {
146 // Already stopped: resume immediately
147
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 19 times.
24x if(env->stop_token.stop_requested())
148 {
149 5x canceled_ = true;
150 5x return h;
151 }
152
153 19x ts_ = &env->executor.context().use_service<detail::timer_service>();
154
155 // Register stop callback before arming the timer.
156 // Once the timer is armed, another thread can fire it,
157 // resume the coroutine, and destroy this awaitable.
158 19x stop_cb_active_ = true;
159 57x ::new(stop_cb_buf_) stop_cb_t(
160 19x env->stop_token,
161 19x cancel_fn{this, env->executor, h});
162
163 // If the stop callback already claimed the resume
164 // (inline invocation), skip the timer entirely.
165
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
19x if(claimed_.load(std::memory_order_acquire))
166 return std::noop_coroutine();
167
168 // Schedule timer using the output-reference overload so
169 // that tid_ is written while the timer_service lock is
170 // held — the timer thread cannot fire the callback until
171 // after the lock is released, at which point tid_ is set.
172 19x ts_->schedule_after(dur_,
173 55x [this, h, ex = env->executor]()
174 {
175
1/2
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
17x if(!claimed_.exchange(
176 true, std::memory_order_acq_rel))
177 {
178 17x ex.post(h);
179 }
180 17x },
181 19x tid_);
182
183 19x return std::noop_coroutine();
184 }
185
186 26x void await_resume() noexcept
187 {
188
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 8 times.
26x if(stop_cb_active_)
189 {
190 18x stop_cb_().~stop_cb_t();
191 18x stop_cb_active_ = false;
192 }
193
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 8 times.
26x if(ts_)
194 18x ts_->cancel(tid_);
195 26x }
196 };
197
198 /** Suspend the current coroutine for a duration.
199
200 Returns an IoAwaitable that completes at or after the
201 specified duration, or earlier if the environment's stop
202 token is activated. Completion is always normal (void
203 return); no exception is thrown on cancellation.
204
205 Zero or negative durations complete synchronously without
206 scheduling a timer.
207
208 @par Example
209 @code
210 co_await delay(std::chrono::milliseconds(100));
211 @endcode
212
213 @param dur The duration to wait.
214
215 @return A @ref delay_awaitable whose `await_resume`
216 returns `void`.
217
218 @throws Nothing.
219
220 @see timeout, delay_awaitable
221 */
222 template<typename Rep, typename Period>
223 delay_awaitable
224 26x delay(std::chrono::duration<Rep, Period> dur) noexcept
225 {
226 return delay_awaitable{
227 26x std::chrono::duration_cast<std::chrono::nanoseconds>(dur)};
228 }
229
230 } // capy
231 } // boost
232
233 #endif
234