libs/corosio/include/boost/corosio/timer.hpp

98.2% Lines (54/55) 100.0% Functions (16/16) 90.3% Branches (28/31)
libs/corosio/include/boost/corosio/timer.hpp
Line Branch Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 // Copyright (c) 2026 Steve Gerbino
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_TIMER_HPP
12 #define BOOST_COROSIO_TIMER_HPP
13
14 #include <boost/corosio/detail/config.hpp>
15 #include <boost/corosio/detail/except.hpp>
16 #include <boost/corosio/io_object.hpp>
17 #include <boost/capy/io_result.hpp>
18 #include <boost/capy/error.hpp>
19 #include <boost/capy/ex/executor_ref.hpp>
20 #include <boost/capy/ex/execution_context.hpp>
21 #include <boost/capy/ex/io_env.hpp>
22 #include <boost/capy/concept/executor.hpp>
23 #include <system_error>
24
25 #include <chrono>
26 #include <coroutine>
27 #include <cstddef>
28 #include <limits>
29 #include <stop_token>
30
31 namespace boost::corosio {
32
33 /** An asynchronous timer for coroutine I/O.
34
35 This class provides asynchronous timer operations that return
36 awaitable types. The timer can be used to schedule operations
37 to occur after a specified duration or at a specific time point.
38
39 Multiple coroutines may wait concurrently on the same timer.
40 When the timer expires, all waiters complete with success. When
41 the timer is cancelled, all waiters complete with an error that
42 compares equal to `capy::cond::canceled`.
43
44 Each timer operation participates in the affine awaitable protocol,
45 ensuring coroutines resume on the correct executor.
46
47 @par Thread Safety
48 Distinct objects: Safe.@n
49 Shared objects: Unsafe.
50
51 @par Semantics
52 Wraps platform timer facilities via the io_context reactor.
53 Operations dispatch to OS timer APIs (timerfd, IOCP timers,
54 kqueue EVFILT_TIMER).
55 */
56 class BOOST_COROSIO_DECL timer : public io_object
57 {
58 struct wait_awaitable
59 {
60 timer& t_;
61 std::stop_token token_;
62 mutable std::error_code ec_;
63
64 9187 explicit wait_awaitable(timer& t) noexcept : t_(t) {}
65
66 9187 bool await_ready() const noexcept
67 {
68 9187 return token_.stop_requested();
69 }
70
71 9187 capy::io_result<> await_resume() const noexcept
72 {
73
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9187 times.
9187 if (token_.stop_requested())
74 return {capy::error::canceled};
75 9187 return {ec_};
76 }
77
78 9187 auto await_suspend(
79 std::coroutine_handle<> h,
80 capy::io_env const* env) -> std::coroutine_handle<>
81 {
82 9187 token_ = env->stop_token;
83 9187 auto& impl = t_.get();
84 // Inline fast path: already expired and not in the heap
85
2/2
✓ Branch 0 taken 9165 times.
✓ Branch 1 taken 22 times.
18352 if (impl.heap_index_ == timer_impl::npos &&
86
5/5
✓ Branch 2 taken 9165 times.
✓ Branch 4 taken 9161 times.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 222 times.
✓ Branch 7 taken 8939 times.
18326 (impl.expiry_ == (time_point::min)() ||
87
3/3
✓ Branch 2 taken 9161 times.
✓ Branch 5 taken 226 times.
✓ Branch 6 taken 8961 times.
18348 impl.expiry_ <= clock_type::now()))
88 {
89 226 ec_ = {};
90 226 auto d = env->executor;
91
1/1
✓ Branch 1 taken 226 times.
226 d.post(h);
92 226 return std::noop_coroutine();
93 }
94
1/1
✓ Branch 2 taken 8961 times.
17922 return impl.wait(
95 17922 h, env->executor, std::move(token_), &ec_);
96 }
97 };
98
99 public:
100 struct timer_impl : io_object_impl
101 {
102 static constexpr std::size_t npos =
103 (std::numeric_limits<std::size_t>::max)();
104
105 std::chrono::steady_clock::time_point expiry_{};
106 std::size_t heap_index_ = npos;
107 bool might_have_pending_waits_ = false;
108
109 virtual std::coroutine_handle<> wait(
110 std::coroutine_handle<>,
111 capy::executor_ref,
112 std::stop_token,
113 std::error_code*) = 0;
114 };
115
116 public:
117 /// The clock type used for time operations.
118 using clock_type = std::chrono::steady_clock;
119
120 /// The time point type for absolute expiry times.
121 using time_point = clock_type::time_point;
122
123 /// The duration type for relative expiry times.
124 using duration = clock_type::duration;
125
126 /** Destructor.
127
128 Cancels any pending operations and releases timer resources.
129 */
130 ~timer();
131
132 /** Construct a timer from an execution context.
133
134 @param ctx The execution context that will own this timer.
135 */
136 explicit timer(capy::execution_context& ctx);
137
138 /** Construct a timer with an initial absolute expiry time.
139
140 @param ctx The execution context that will own this timer.
141 @param t The initial expiry time point.
142 */
143 timer(capy::execution_context& ctx, time_point t);
144
145 /** Construct a timer with an initial relative expiry time.
146
147 @param ctx The execution context that will own this timer.
148 @param d The initial expiry duration relative to now.
149 */
150 template<class Rep, class Period>
151 2 timer(
152 capy::execution_context& ctx,
153 std::chrono::duration<Rep, Period> d)
154 2 : timer(ctx)
155 {
156
1/1
✓ Branch 1 taken 2 times.
2 expires_after(d);
157 2 }
158
159 /** Move constructor.
160
161 Transfers ownership of the timer resources.
162
163 @param other The timer to move from.
164 */
165 timer(timer&& other) noexcept;
166
167 /** Move assignment operator.
168
169 Closes any existing timer and transfers ownership.
170 The source and destination must share the same execution context.
171
172 @param other The timer to move from.
173
174 @return Reference to this timer.
175
176 @throws std::logic_error if the timers have different execution contexts.
177 */
178 timer& operator=(timer&& other);
179
180 timer(timer const&) = delete;
181 timer& operator=(timer const&) = delete;
182
183 /** Cancel all pending asynchronous wait operations.
184
185 All outstanding operations complete with an error code that
186 compares equal to `capy::cond::canceled`.
187
188 @return The number of operations that were cancelled.
189 */
190 20 std::size_t cancel()
191 {
192
2/2
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 8 times.
20 if (!get().might_have_pending_waits_)
193 12 return 0;
194 8 return do_cancel();
195 }
196
197 /** Cancel one pending asynchronous wait operation.
198
199 The oldest pending wait is cancelled (FIFO order). It
200 completes with an error code that compares equal to
201 `capy::cond::canceled`.
202
203 @return The number of operations that were cancelled (0 or 1).
204 */
205 4 std::size_t cancel_one()
206 {
207
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
4 if (!get().might_have_pending_waits_)
208 2 return 0;
209 2 return do_cancel_one();
210 }
211
212 /** Return the timer's expiry time as an absolute time.
213
214 @return The expiry time point. If no expiry has been set,
215 returns a default-constructed time_point.
216 */
217 34 time_point expiry() const noexcept
218 {
219 34 return get().expiry_;
220 }
221
222 /** Set the timer's expiry time as an absolute time.
223
224 Any pending asynchronous wait operations will be cancelled.
225
226 @param t The expiry time to be used for the timer.
227
228 @return The number of pending operations that were cancelled.
229 */
230 18 std::size_t expires_at(time_point t)
231 {
232 18 auto& impl = get();
233 18 impl.expiry_ = t;
234
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
18 if (impl.heap_index_ == timer_impl::npos &&
235
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 !impl.might_have_pending_waits_)
236 16 return 0;
237 2 return do_update_expiry();
238 }
239
240 /** Set the timer's expiry time relative to now.
241
242 Any pending asynchronous wait operations will be cancelled.
243
244 @param d The expiry time relative to now.
245
246 @return The number of pending operations that were cancelled.
247 */
248 9187 std::size_t expires_after(duration d)
249 {
250 9187 auto& impl = get();
251
3/3
✓ Branch 2 taken 9187 times.
✓ Branch 5 taken 6 times.
✓ Branch 6 taken 9181 times.
9187 if (d <= duration::zero())
252 6 impl.expiry_ = (time_point::min)();
253 else
254
1/1
✓ Branch 2 taken 9181 times.
9181 impl.expiry_ = clock_type::now() + d;
255
2/2
✓ Branch 0 taken 9183 times.
✓ Branch 1 taken 4 times.
9187 if (impl.heap_index_ == timer_impl::npos &&
256
1/2
✓ Branch 0 taken 9183 times.
✗ Branch 1 not taken.
9183 !impl.might_have_pending_waits_)
257 9183 return 0;
258 4 return do_update_expiry();
259 }
260
261 /** Set the timer's expiry time relative to now.
262
263 This is a convenience overload that accepts any duration type
264 and converts it to the timer's native duration type. Any
265 pending asynchronous wait operations will be cancelled.
266
267 @param d The expiry time relative to now.
268
269 @return The number of pending operations that were cancelled.
270 */
271 template<class Rep, class Period>
272 9187 std::size_t expires_after(std::chrono::duration<Rep, Period> d)
273 {
274 9187 return expires_after(std::chrono::duration_cast<duration>(d));
275 }
276
277 /** Wait for the timer to expire.
278
279 Multiple coroutines may wait on the same timer concurrently.
280 When the timer expires, all waiters complete with success.
281
282 The operation supports cancellation via `std::stop_token` through
283 the affine awaitable protocol. If the associated stop token is
284 triggered, only that waiter completes with an error that
285 compares equal to `capy::cond::canceled`; other waiters are
286 unaffected.
287
288 @par Example
289 @code
290 timer t(ctx);
291 t.expires_after(std::chrono::seconds(5));
292 auto [ec] = co_await t.wait();
293 if (ec == capy::cond::canceled)
294 {
295 // Cancelled via stop_token or cancel()
296 co_return;
297 }
298 if (ec)
299 {
300 // Handle other errors
301 co_return;
302 }
303 // Timer expired
304 @endcode
305
306 @return An awaitable that completes with `io_result<>`.
307 Returns success (default error_code) when the timer expires,
308 or an error code on failure. Compare against error conditions
309 (e.g., `ec == capy::cond::canceled`) rather than error codes.
310
311 @par Preconditions
312 The timer must have an expiry time set via expires_at() or
313 expires_after().
314 */
315 9187 auto wait()
316 {
317 9187 return wait_awaitable(*this);
318 }
319
320 private:
321 // Out-of-line cancel/expiry when inline fast-path
322 // conditions (no waiters, not in heap) are not met.
323 std::size_t do_cancel();
324 std::size_t do_cancel_one();
325 std::size_t do_update_expiry();
326
327 /// Return the underlying implementation.
328 27664 timer_impl& get() const noexcept
329 {
330 27664 return *static_cast<timer_impl*>(impl_);
331 }
332 };
333
334 } // namespace boost::corosio
335
336 #endif
337