include/boost/corosio/detail/timer.hpp

95.3% Lines (41/43) 100.0% List of functions (9/9)
timer.hpp
f(x) Functions (9)
Line TLA 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_DETAIL_TIMER_HPP
12 #define BOOST_COROSIO_DETAIL_TIMER_HPP
13
14 #include <boost/corosio/detail/config.hpp>
15 #include <boost/corosio/detail/intrusive.hpp>
16 #include <boost/corosio/io/io_object.hpp>
17 #include <boost/capy/continuation.hpp>
18 #include <boost/capy/io_result.hpp>
19 #include <boost/capy/error.hpp>
20 #include <boost/capy/ex/executor_ref.hpp>
21 #include <boost/capy/ex/execution_context.hpp>
22 #include <boost/capy/ex/io_env.hpp>
23 #include <boost/capy/concept/executor.hpp>
24
25 #include <atomic>
26 #include <chrono>
27 #include <concepts>
28 #include <coroutine>
29 #include <cstddef>
30 #include <limits>
31 #include <stop_token>
32 #include <system_error>
33 #include <type_traits>
34
35 namespace boost::corosio::detail {
36
37 // Defined in timer_service.hpp, which includes this header. The
38 // implementation::wait() body needs waiter_node and timer_service to
39 // be complete, so it is declared here and defined there; intrusive_list
40 // only stores waiter_node pointers, so the forward declaration suffices
41 // for implementation's data layout.
42 class timer_service;
43 struct waiter_node;
44
45 /** An asynchronous timer for coroutine I/O.
46
47 This class provides asynchronous timer operations that return
48 awaitable types. The timer can be used to schedule operations
49 to occur after a specified duration or at a specific time point.
50
51 Multiple coroutines may wait concurrently on the same timer.
52 When the timer expires, all waiters complete with success. When
53 the timer is cancelled, all waiters complete with an error that
54 compares equal to `capy::cond::canceled`.
55
56 Each timer operation participates in the affine awaitable protocol,
57 ensuring coroutines resume on the correct executor.
58
59 @par Thread Safety
60 Distinct objects: Safe.@n
61 Shared objects: Unsafe.
62
63 @par Semantics
64 Timers are not backed by per-timer kernel objects. The io_context's
65 timer service keeps a process-side min-heap of pending expirations;
66 the nearest expiry drives the reactor's poll timeout, and expirations
67 are processed in the run loop.
68 */
69 class BOOST_COROSIO_DECL timer : public io_object
70 {
71 struct wait_awaitable
72 {
73 timer& t_;
74 std::error_code ec_;
75 capy::continuation cont_;
76
77 8457x explicit wait_awaitable(timer& t) noexcept : t_(t) {}
78
79 2040x bool await_ready() const noexcept
80 {
81 2040x return false;
82 }
83
84 // Cancellation surfaces through ec_: the stop_token path in
85 // wait() completes the waiter with error::canceled written to
86 // ec_, so there is no separate token to consult here.
87 8431x capy::io_result<> await_resume() const noexcept
88 {
89 8431x return {ec_};
90 }
91
92 8457x auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
93 -> std::coroutine_handle<>
94 {
95 8457x cont_.h = h;
96 8457x auto& impl = t_.get();
97 // Inline fast path: already expired and not in the heap
98 8457x if (impl.heap_index_.load(std::memory_order_relaxed) ==
99 16914x implementation::npos &&
100 16250x (impl.expiry_ == (time_point::min)() ||
101 16250x impl.expiry_ <= clock_type::now()))
102 {
103 856x ec_ = {};
104 856x auto d = env->executor;
105 856x d.post(cont_);
106 856x return std::noop_coroutine();
107 }
108 7601x return impl.wait(h, env->executor, env->stop_token, &ec_, &cont_);
109 }
110 };
111
112 public:
113 /** Backend state and wait entry point for a timer.
114
115 Holds per-timer state (expiry, heap position, waiter list) and
116 the `wait` entry point used by the awaitable returned from
117 @ref timer::wait. There is exactly one concrete timer backend,
118 so `wait` is a plain member function rather than a virtual
119 dispatch point.
120 */
121 struct implementation : io_object::implementation
122 {
123 /// Sentinel value indicating the timer is not in the heap.
124 static constexpr std::size_t npos =
125 (std::numeric_limits<std::size_t>::max)();
126
127 // Only mutated by the owning thread (expires_at/expires_after)
128 // before a wait is published; cross-thread consumers read the
129 // heap entry's copied time_, never this field, so it needs no
130 // atomicity.
131 /// The absolute expiry time point.
132 std::chrono::steady_clock::time_point expiry_{};
133
134 // heap_index_ and might_have_pending_waits_ are cross-thread
135 // hints, not authoritative state: the real state lives in the
136 // heap and waiter list under timer_service::mutex_. Every
137 // unlocked fast-out that reads them is either re-validated under
138 // the mutex or safe under a stale value in both directions, and
139 // any locked writer / locked reader pair is already ordered by
140 // the mutex. All accesses therefore use memory_order_relaxed,
141 // which keeps the lock-free fast paths fence-free while making
142 // the concurrent reads well-defined.
143 /// Index in the timer service's min-heap, or `npos`.
144 std::atomic<std::size_t> heap_index_{npos};
145
146 /// True if `wait()` has been called since last cancel.
147 std::atomic<bool> might_have_pending_waits_{false};
148
149 /// The timer service that owns this implementation.
150 timer_service* svc_ = nullptr;
151
152 /// Coroutines currently waiting on this timer's expiry.
153 intrusive_list<waiter_node> waiters_;
154
155 /// Free list linkage, reused when this impl is recycled.
156 implementation* next_free_ = nullptr;
157
158 /// Construct bound to the given timer service.
159 293x explicit implementation(timer_service& svc) noexcept : svc_(&svc) {}
160
161 /// Initiate an asynchronous wait for the timer to expire.
162 // Exported at member level: dllexport on the enclosing timer
163 // class does not extend to nested classes, and header-inline
164 // callers (wait_awaitable::await_suspend) reference this
165 // symbol from outside the corosio DLL.
166 BOOST_COROSIO_DECL
167 std::coroutine_handle<> wait(
168 std::coroutine_handle<>,
169 capy::executor_ref,
170 std::stop_token,
171 std::error_code*,
172 capy::continuation*);
173 };
174
175 /// The clock type used for time operations.
176 using clock_type = std::chrono::steady_clock;
177
178 /// The time point type for absolute expiry times.
179 using time_point = clock_type::time_point;
180
181 /// The duration type for relative expiry times.
182 using duration = clock_type::duration;
183
184 /** Destructor.
185
186 Cancels any pending operations and releases timer resources.
187 */
188 ~timer() override;
189
190 /** Construct a timer from an execution context.
191
192 @param ctx The execution context that will own this timer. It
193 must be a corosio io_context; otherwise the constructor
194 throws (a timer service is required).
195
196 @throws std::logic_error if @p ctx is not an io_context.
197 */
198 explicit timer(capy::execution_context& ctx);
199
200 /** Construct a timer with an initial absolute expiry time.
201
202 @param ctx The execution context that will own this timer. It
203 must be a corosio io_context; otherwise the constructor
204 throws (a timer service is required).
205 @param t The initial expiry time point.
206
207 @throws std::logic_error if @p ctx is not an io_context.
208 */
209 timer(capy::execution_context& ctx, time_point t);
210
211 /** Construct a timer with an initial relative expiry time.
212
213 @param ctx The execution context that will own this timer. It
214 must be a corosio io_context; otherwise the constructor
215 throws (a timer service is required).
216 @param d The initial expiry duration relative to now.
217
218 @throws std::logic_error if @p ctx is not an io_context.
219 */
220 template<class Rep, class Period>
221 timer(capy::execution_context& ctx, std::chrono::duration<Rep, Period> d)
222 : timer(ctx)
223 {
224 expires_after(d);
225 }
226
227 /** Construct a timer from an executor.
228
229 The timer is associated with the executor's context, which must
230 be a corosio io_context.
231
232 @param ex The executor whose context will own this timer.
233
234 @throws std::logic_error if the executor's context is not an
235 io_context.
236 */
237 template<class Ex>
238 requires(!std::same_as<std::remove_cvref_t<Ex>, timer>) &&
239 capy::Executor<Ex>
240 explicit timer(Ex const& ex) : timer(ex.context())
241 {
242 }
243
244 /** Construct a timer from an executor with an absolute expiry time.
245
246 @param ex The executor whose context will own this timer.
247 @param t The initial expiry time point.
248
249 @throws std::logic_error if the executor's context is not an
250 io_context.
251 */
252 template<class Ex>
253 requires capy::Executor<Ex>
254 timer(Ex const& ex, time_point t) : timer(ex.context(), t)
255 {
256 }
257
258 /** Construct a timer from an executor with a relative expiry time.
259
260 @param ex The executor whose context will own this timer.
261 @param d The initial expiry duration relative to now.
262
263 @throws std::logic_error if the executor's context is not an
264 io_context.
265 */
266 template<class Ex, class Rep, class Period>
267 requires capy::Executor<Ex>
268 timer(Ex const& ex, std::chrono::duration<Rep, Period> d)
269 : timer(ex.context(), d)
270 {
271 }
272
273 /** Move constructor.
274
275 Transfers ownership of the timer resources.
276
277 @param other The timer to move from.
278
279 @pre No awaitables returned by @p other's methods exist.
280 @pre The execution context associated with @p other must
281 outlive this timer.
282 */
283 timer(timer&& other) noexcept;
284
285 /** Move assignment operator.
286
287 Closes any existing timer and transfers ownership.
288
289 @param other The timer to move from.
290
291 @pre No awaitables returned by either `*this` or @p other's
292 methods exist.
293 @pre The execution context associated with @p other must
294 outlive this timer.
295
296 @return Reference to this timer.
297 */
298 timer& operator=(timer&& other) noexcept;
299
300 timer(timer const&) = delete;
301 timer& operator=(timer const&) = delete;
302
303 /** Cancel all pending asynchronous wait operations.
304
305 All outstanding operations complete with an error code that
306 compares equal to `capy::cond::canceled`.
307
308 @return The number of operations that were cancelled.
309 */
310 std::size_t cancel()
311 {
312 if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))
313 return 0;
314 return do_cancel();
315 }
316
317 /** Cancel one pending asynchronous wait operation.
318
319 The oldest pending wait is cancelled (FIFO order). It
320 completes with an error code that compares equal to
321 `capy::cond::canceled`.
322
323 @return The number of operations that were cancelled (0 or 1).
324 */
325 std::size_t cancel_one()
326 {
327 if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))
328 return 0;
329 return do_cancel_one();
330 }
331
332 /** Return the timer's expiry time as an absolute time.
333
334 @return The expiry time point. If no expiry has been set,
335 returns a default-constructed time_point.
336 */
337 time_point expiry() const noexcept
338 {
339 return get().expiry_;
340 }
341
342 /** Set the timer's expiry time as an absolute time.
343
344 Any pending asynchronous wait operations will be cancelled.
345
346 @param t The expiry time to be used for the timer.
347
348 @return The number of pending operations that were cancelled.
349 */
350 6x std::size_t expires_at(time_point t)
351 {
352 6x auto& impl = get();
353 6x impl.expiry_ = t;
354 6x if (impl.heap_index_.load(std::memory_order_relaxed) ==
355 12x implementation::npos &&
356 6x !impl.might_have_pending_waits_.load(std::memory_order_relaxed))
357 6x return 0;
358 return do_update_expiry();
359 }
360
361 /** Set the timer's expiry time relative to now.
362
363 Any pending asynchronous wait operations will be cancelled.
364
365 @param d The expiry time relative to now.
366
367 @return The number of pending operations that were cancelled.
368 */
369 8451x std::size_t expires_after(duration d)
370 {
371 8451x auto& impl = get();
372 8451x if (d <= duration::zero())
373 664x impl.expiry_ = (time_point::min)();
374 else
375 {
376 // Saturate rather than overflow: a clamped near-max duration
377 // (e.g. delay(hours::max())) would wrap now() + d past the
378 // clock's range and appear already elapsed.
379 7787x auto const now = clock_type::now();
380 7787x impl.expiry_ = ((time_point::max)() - now < d)
381 15570x ? (time_point::max)()
382 7783x : now + d;
383 }
384 8451x if (impl.heap_index_.load(std::memory_order_relaxed) ==
385 16902x implementation::npos &&
386 8451x !impl.might_have_pending_waits_.load(std::memory_order_relaxed))
387 8451x return 0;
388 return do_update_expiry();
389 }
390
391 /** Set the timer's expiry time relative to now.
392
393 This is a convenience overload that accepts any duration type
394 and converts it to the timer's native duration type. Any
395 pending asynchronous wait operations will be cancelled.
396
397 @param d The expiry time relative to now.
398
399 @return The number of pending operations that were cancelled.
400 */
401 template<class Rep, class Period>
402 std::size_t expires_after(std::chrono::duration<Rep, Period> d)
403 {
404 return expires_after(std::chrono::duration_cast<duration>(d));
405 }
406
407 /** Wait for the timer to expire.
408
409 Multiple coroutines may wait on the same timer concurrently.
410 When the timer expires, all waiters complete with success.
411
412 The operation supports cancellation via `std::stop_token` through
413 the affine awaitable protocol. If the associated stop token is
414 triggered, only that waiter completes with an error that
415 compares equal to `capy::cond::canceled`; other waiters are
416 unaffected.
417
418 This timer must outlive the returned awaitable.
419
420 @return An awaitable that completes with `io_result<>`.
421 */
422 8457x auto wait()
423 {
424 8457x return wait_awaitable(*this);
425 }
426
427 protected:
428 explicit timer(handle h) noexcept : io_object(std::move(h)) {}
429
430 private:
431 // Defined in src/corosio/src/timer.cpp, which includes both this
432 // header and timer_service.hpp, so the timer_service_* free
433 // functions are visible there.
434 std::size_t do_cancel();
435 std::size_t do_cancel_one();
436 std::size_t do_update_expiry();
437
438 /// Return the underlying implementation.
439 16914x implementation& get() const noexcept
440 {
441 16914x return *static_cast<implementation*>(h_.get());
442 }
443 };
444
445 } // namespace boost::corosio::detail
446
447 #endif
448