include/boost/capy/delay.hpp

100.0% Lines (50/50) 100.0% List of functions (9/9) 85.7% Branches (12/14)
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 BOOST_CAPY_MSVC_WARNING_PUSH
92 BOOST_CAPY_MSVC_WARNING_DISABLE(4324)
93 alignas(stop_cb_t)
94 unsigned char stop_cb_buf_[sizeof(stop_cb_t)];
95 BOOST_CAPY_MSVC_WARNING_POP
96
97 19x stop_cb_t& stop_cb_() noexcept
98 {
99 19x return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_);
100 }
101
102 public:
103 27x explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
104 27x : dur_(dur)
105 {
106 27x }
107
108 /// @pre The stop callback must not be active
109 /// (i.e. the object has not yet been awaited).
110 58x delay_awaitable(delay_awaitable&& o) noexcept
111 58x : dur_(o.dur_)
112 58x , ts_(o.ts_)
113 58x , tid_(o.tid_)
114 58x , claimed_(o.claimed_.load(std::memory_order_relaxed))
115 58x , canceled_(o.canceled_)
116 58x , stop_cb_active_(std::exchange(o.stop_cb_active_, false))
117 {
118 58x }
119
120 85x ~delay_awaitable()
121 {
122
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 84 times.
85x if(stop_cb_active_)
123 1x stop_cb_().~stop_cb_t();
124
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 66 times.
85x if(ts_)
125 19x ts_->cancel(tid_);
126 85x }
127
128 delay_awaitable(delay_awaitable const&) = delete;
129 delay_awaitable& operator=(delay_awaitable const&) = delete;
130 delay_awaitable& operator=(delay_awaitable&&) = delete;
131
132 26x bool await_ready() const noexcept
133 {
134 26x return dur_.count() <= 0;
135 }
136
137 std::coroutine_handle<>
138 24x await_suspend(
139 std::coroutine_handle<> h,
140 io_env const* env) noexcept
141 {
142 // Already stopped: resume immediately
143
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 19 times.
24x if(env->stop_token.stop_requested())
144 {
145 5x canceled_ = true;
146 5x return h;
147 }
148
149 19x ts_ = &env->executor.context().use_service<detail::timer_service>();
150
151 // Schedule timer (won't fire inline since deadline is in the future)
152 19x tid_ = ts_->schedule_after(dur_,
153 19x [this, h, ex = env->executor]()
154 {
155
1/2
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
17x if(!claimed_.exchange(
156 true, std::memory_order_acq_rel))
157 {
158 17x ex.post(h);
159 }
160 17x });
161
162 // Register stop callback (may fire inline)
163 57x ::new(stop_cb_buf_) stop_cb_t(
164 19x env->stop_token,
165 19x cancel_fn{this, env->executor, h});
166 19x stop_cb_active_ = true;
167
168 19x return std::noop_coroutine();
169 }
170
171 26x void await_resume() noexcept
172 {
173
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 8 times.
26x if(stop_cb_active_)
174 {
175 18x stop_cb_().~stop_cb_t();
176 18x stop_cb_active_ = false;
177 }
178
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 8 times.
26x if(ts_)
179 18x ts_->cancel(tid_);
180 26x }
181 };
182
183 /** Suspend the current coroutine for a duration.
184
185 Returns an IoAwaitable that completes at or after the
186 specified duration, or earlier if the environment's stop
187 token is activated. Completion is always normal (void
188 return); no exception is thrown on cancellation.
189
190 Zero or negative durations complete synchronously without
191 scheduling a timer.
192
193 @par Example
194 @code
195 co_await delay(std::chrono::milliseconds(100));
196 @endcode
197
198 @param dur The duration to wait.
199
200 @return A @ref delay_awaitable whose `await_resume`
201 returns `void`.
202
203 @throws Nothing.
204
205 @see timeout, delay_awaitable
206 */
207 template<typename Rep, typename Period>
208 delay_awaitable
209 26x delay(std::chrono::duration<Rep, Period> dur) noexcept
210 {
211 return delay_awaitable{
212 26x std::chrono::duration_cast<std::chrono::nanoseconds>(dur)};
213 }
214
215 } // capy
216 } // boost
217
218 #endif
219