include/boost/corosio/timer.hpp

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