Line data 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 9187 : if (token_.stop_requested())
74 0 : 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 18352 : if (impl.heap_index_ == timer_impl::npos &&
86 18326 : (impl.expiry_ == (time_point::min)() ||
87 18348 : impl.expiry_ <= clock_type::now()))
88 : {
89 226 : ec_ = {};
90 226 : auto d = env->executor;
91 226 : d.post(h);
92 226 : return std::noop_coroutine();
93 : }
94 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 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 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 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 18 : if (impl.heap_index_ == timer_impl::npos &&
235 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 9187 : if (d <= duration::zero())
252 6 : impl.expiry_ = (time_point::min)();
253 : else
254 9181 : impl.expiry_ = clock_type::now() + d;
255 9187 : if (impl.heap_index_ == timer_impl::npos &&
256 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
|