include/boost/corosio/detail/timer.hpp

96.8% Lines (61/63) 100.0% List of functions (15/15) 76.7% Branches (23/30)
timer.hpp
f(x) Functions (15)
Function Calls Lines Branches Blocks
boost::corosio::detail::timer::implementation::implementation(boost::corosio::detail::timer_service&) :124 154x 100.0% 100.0% boost::corosio::detail::timer::implementation::already_expired() const :133 5623x 100.0% 83.3% 77.8% boost::corosio::detail::timer::expires_at(std::chrono::time_point<std::chrono::_V2::steady_clock, std::chrono::duration<long long, std::ratio<1ll, 1000000000ll> > >) :337 3x 87.5% 50.0% 65.4% boost::corosio::detail::timer::expires_after(std::chrono::duration<long long, std::ratio<1ll, 1000000000ll> >) :356 3179x 92.3% 78.6% 71.1% boost::corosio::detail::timer::get() const :424 6364x 100.0% 100.0% boost::corosio::detail::waiter_node::completion_op::completion_op() :461 6364x 100.0% 100.0% boost::corosio::detail::waiter_node::waiter_node() :512 6364x 100.0% 100.0% boost::corosio::detail::waiter_node::arm_stop_cb() :527 1432x 100.0% 100.0% boost::corosio::detail::waiter_node::reset_stop_cb() :534 1869x 100.0% 100.0% 100.0% boost::corosio::detail::wait_awaitable::wait_awaitable(boost::corosio::detail::timer&) :557 3182x 100.0% 100.0% boost::corosio::detail::wait_awaitable::wait_awaitable(boost::corosio::detail::wait_awaitable&&) :559 3182x 100.0% 100.0% boost::corosio::detail::wait_awaitable::await_ready() const :565 1020x 100.0% 100.0% boost::corosio::detail::wait_awaitable::await_resume() const :573 3168x 100.0% 100.0% boost::corosio::detail::wait_awaitable::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :578 3182x 100.0% 100.0% 100.0% boost::corosio::detail::timer::wait() :600 3182x 100.0% 100.0%
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco ([email protected])
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/detail/scheduler_op.hpp>
17 #include <boost/corosio/io/io_object.hpp>
18 #include <boost/capy/continuation.hpp>
19 #include <boost/capy/io_result.hpp>
20 #include <boost/capy/error.hpp>
21 #include <boost/capy/ex/executor_ref.hpp>
22 #include <boost/capy/ex/execution_context.hpp>
23 #include <boost/capy/ex/io_env.hpp>
24 #include <boost/capy/concept/executor.hpp>
25
26 #include <atomic>
27 #include <chrono>
28 #include <concepts>
29 #include <coroutine>
30 #include <cstddef>
31 #include <limits>
32 #include <new>
33 #include <stop_token>
34 #include <system_error>
35 #include <type_traits>
36
37 namespace boost::corosio::detail {
38
39 // timer_service is defined in timer_service.hpp, which includes this
40 // header. waiter_node and wait_awaitable are defined below the timer
41 // class: waiter_node stores a timer::implementation*, which cannot be
42 // forward-declared as a nested type. intrusive_list only stores
43 // waiter_node pointers, so this forward declaration suffices for
44 // implementation's data layout.
45 class timer_service;
46 struct waiter_node;
47 struct wait_awaitable;
48
49 /** An asynchronous timer for coroutine I/O.
50
51 This class provides asynchronous timer operations that return
52 awaitable types. The timer can be used to schedule operations
53 to occur after a specified duration or at a specific time point.
54
55 Multiple coroutines may wait concurrently on the same timer.
56 When the timer expires, all waiters complete with success. When
57 the timer is cancelled, all waiters complete with an error that
58 compares equal to `capy::cond::canceled`.
59
60 Each timer operation participates in the affine awaitable protocol,
61 ensuring coroutines resume on the correct executor.
62
63 @par Thread Safety
64 Distinct objects: Safe.@n
65 Shared objects: Unsafe.
66
67 @par Semantics
68 Timers are not backed by per-timer kernel objects. The io_context's
69 timer service keeps a process-side min-heap of pending expirations;
70 the nearest expiry drives the reactor's poll timeout, and expirations
71 are processed in the run loop.
72 */
73 class BOOST_COROSIO_DECL timer : public io_object
74 {
75 friend struct wait_awaitable;
76
77 public:
78 /** Backend state and wait entry point for a timer.
79
80 Holds per-timer state (expiry, heap position, waiter list) and
81 the `wait` entry point used by the awaitable returned from
82 @ref timer::wait. There is exactly one concrete timer backend,
83 so `wait` is a plain member function rather than a virtual
84 dispatch point.
85 */
86 struct implementation : io_object::implementation
87 {
88 /// Sentinel value indicating the timer is not in the heap.
89 static constexpr std::size_t npos =
90 (std::numeric_limits<std::size_t>::max)();
91
92 // Only mutated by the owning thread (expires_at/expires_after)
93 // before a wait is published; cross-thread consumers read the
94 // heap entry's copied time_, never this field, so it needs no
95 // atomicity.
96 /// The absolute expiry time point.
97 std::chrono::steady_clock::time_point expiry_{};
98
99 // heap_index_ and might_have_pending_waits_ are cross-thread
100 // hints, not authoritative state: the real state lives in the
101 // heap and waiter list under timer_service::mutex_. Every
102 // unlocked fast-out that reads them is either re-validated under
103 // the mutex or safe under a stale value in both directions, and
104 // any locked writer / locked reader pair is already ordered by
105 // the mutex. All accesses therefore use memory_order_relaxed,
106 // which keeps the lock-free fast paths fence-free while making
107 // the concurrent reads well-defined.
108 /// Index in the timer service's min-heap, or `npos`.
109 std::atomic<std::size_t> heap_index_{npos};
110
111 /// True if `wait()` has been called since last cancel.
112 std::atomic<bool> might_have_pending_waits_{false};
113
114 /// The timer service that owns this implementation.
115 timer_service* svc_ = nullptr;
116
117 /// Coroutines currently waiting on this timer's expiry.
118 intrusive_list<waiter_node> waiters_;
119
120 /// Free list linkage, reused when this impl is recycled.
121 implementation* next_free_ = nullptr;
122
123 /// Construct bound to the given timer service.
124 154x explicit implementation(timer_service& svc) noexcept : svc_(&svc) {}
125
126 /** Check whether the timer is expired and absent from the heap.
127
128 The single definition of the already-expired fast-path
129 predicate: `await_suspend` tests it inline and `wait()`
130 re-tests it because the expiry can elapse between the two
131 reads.
132 */
133 5623x bool already_expired() const noexcept
134 {
135
1/2
✓ Branch 17 → 18 taken 5623 times.
✗ Branch 17 → 26 not taken.
16869x return heap_index_.load(std::memory_order_relaxed) == npos &&
136
2/2
✓ Branch 20 → 21 taken 5291 times.
✓ Branch 20 → 25 taken 332 times.
5623x (expiry_ ==
137
2/2
✓ Branch 24 → 25 taken 981 times.
✓ Branch 24 → 26 taken 4310 times.
10914x (std::chrono::steady_clock::time_point::min)() ||
138 10914x expiry_ <= std::chrono::steady_clock::now());
139 }
140
141 /** Asynchronously wait for the timer to expire.
142
143 Publishes the waiter into the service's heap and waiter
144 list, after which it may complete on any thread. If the
145 timer is already expired and not in the heap, completes
146 by posting the continuation without publishing.
147
148 @par Preconditions
149 @p w is fully initialized, and its storage (the awaitable
150 on the suspended coroutine's frame) outlives the wait.
151
152 @param w The waiter to publish.
153 */
154 // Exported at member level: dllexport on the enclosing timer
155 // class does not extend to nested classes, and header-inline
156 // callers (wait_awaitable::await_suspend) reference this
157 // symbol from outside the corosio DLL.
158 BOOST_COROSIO_DECL
159 std::coroutine_handle<> wait(waiter_node& w);
160 };
161
162 /// The clock type used for time operations.
163 using clock_type = std::chrono::steady_clock;
164
165 /// The time point type for absolute expiry times.
166 using time_point = clock_type::time_point;
167
168 /// The duration type for relative expiry times.
169 using duration = clock_type::duration;
170
171 /** Destructor.
172
173 Cancels any pending operations and releases timer resources.
174 */
175 ~timer() override;
176
177 /** Construct a timer from an execution context.
178
179 @param ctx The execution context that will own this timer. It
180 must be a corosio io_context; otherwise the constructor
181 throws (a timer service is required).
182
183 @throws std::logic_error if @p ctx is not an io_context.
184 */
185 explicit timer(capy::execution_context& ctx);
186
187 /** Construct a timer with an initial absolute expiry time.
188
189 @param ctx The execution context that will own this timer. It
190 must be a corosio io_context; otherwise the constructor
191 throws (a timer service is required).
192 @param t The initial expiry time point.
193
194 @throws std::logic_error if @p ctx is not an io_context.
195 */
196 timer(capy::execution_context& ctx, time_point t);
197
198 /** Construct a timer with an initial relative expiry time.
199
200 @param ctx The execution context that will own this timer. It
201 must be a corosio io_context; otherwise the constructor
202 throws (a timer service is required).
203 @param d The initial expiry duration relative to now.
204
205 @throws std::logic_error if @p ctx is not an io_context.
206 */
207 template<class Rep, class Period>
208 timer(capy::execution_context& ctx, std::chrono::duration<Rep, Period> d)
209 : timer(ctx)
210 {
211 expires_after(d);
212 }
213
214 /** Construct a timer from an executor.
215
216 The timer is associated with the executor's context, which must
217 be a corosio io_context.
218
219 @param ex The executor whose context will own this timer.
220
221 @throws std::logic_error if the executor's context is not an
222 io_context.
223 */
224 template<class Ex>
225 requires(!std::same_as<std::remove_cvref_t<Ex>, timer>) &&
226 capy::Executor<Ex>
227 explicit timer(Ex const& ex) : timer(ex.context())
228 {
229 }
230
231 /** Construct a timer from an executor with an absolute expiry time.
232
233 @param ex The executor whose context will own this timer.
234 @param t The initial expiry time point.
235
236 @throws std::logic_error if the executor's context is not an
237 io_context.
238 */
239 template<class Ex>
240 requires capy::Executor<Ex>
241 timer(Ex const& ex, time_point t) : timer(ex.context(), t)
242 {
243 }
244
245 /** Construct a timer from an executor with a relative expiry time.
246
247 @param ex The executor whose context will own this timer.
248 @param d The initial expiry duration relative to now.
249
250 @throws std::logic_error if the executor's context is not an
251 io_context.
252 */
253 template<class Ex, class Rep, class Period>
254 requires capy::Executor<Ex>
255 timer(Ex const& ex, std::chrono::duration<Rep, Period> d)
256 : timer(ex.context(), d)
257 {
258 }
259
260 /** Move constructor.
261
262 Transfers ownership of the timer resources.
263
264 @param other The timer to move from.
265
266 @pre No awaitables returned by @p other's methods exist.
267 @pre The execution context associated with @p other must
268 outlive this timer.
269 */
270 timer(timer&& other) noexcept;
271
272 /** Move assignment operator.
273
274 Closes any existing timer and transfers ownership.
275
276 @param other The timer to move from.
277
278 @pre No awaitables returned by either `*this` or @p other's
279 methods exist.
280 @pre The execution context associated with @p other must
281 outlive this timer.
282
283 @return Reference to this timer.
284 */
285 timer& operator=(timer&& other) noexcept;
286
287 timer(timer const&) = delete;
288 timer& operator=(timer const&) = delete;
289
290 /** Cancel all pending asynchronous wait operations.
291
292 All outstanding operations complete with an error code that
293 compares equal to `capy::cond::canceled`.
294
295 @return The number of operations that were cancelled.
296 */
297 std::size_t cancel()
298 {
299 if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))
300 return 0;
301 return do_cancel();
302 }
303
304 /** Cancel one pending asynchronous wait operation.
305
306 The oldest pending wait is cancelled (FIFO order). It
307 completes with an error code that compares equal to
308 `capy::cond::canceled`.
309
310 @return The number of operations that were cancelled (0 or 1).
311 */
312 std::size_t cancel_one()
313 {
314 if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))
315 return 0;
316 return do_cancel_one();
317 }
318
319 /** Return the timer's expiry time as an absolute time.
320
321 @return The expiry time point. If no expiry has been set,
322 returns a default-constructed time_point.
323 */
324 time_point expiry() const noexcept
325 {
326 return get().expiry_;
327 }
328
329 /** Set the timer's expiry time as an absolute time.
330
331 Any pending asynchronous wait operations will be cancelled.
332
333 @param t The expiry time to be used for the timer.
334
335 @return The number of pending operations that were cancelled.
336 */
337 3x std::size_t expires_at(time_point t)
338 {
339 3x auto& impl = get();
340 3x impl.expiry_ = t;
341 3x if (impl.heap_index_.load(std::memory_order_relaxed) ==
342
2/4
✓ Branch 18 → 19 taken 3 times.
✗ Branch 18 → 22 not taken.
✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 25 not taken.
6x implementation::npos &&
343
1/2
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 22 not taken.
3x !impl.might_have_pending_waits_.load(std::memory_order_relaxed))
344 3x return 0;
345 return do_update_expiry();
346 }
347
348 /** Set the timer's expiry time relative to now.
349
350 Any pending asynchronous wait operations will be cancelled.
351
352 @param d The expiry time relative to now.
353
354 @return The number of pending operations that were cancelled.
355 */
356 3179x std::size_t expires_after(duration d)
357 {
358 3179x auto& impl = get();
359
3/3
✓ Branch 4 → 5 taken 3179 times.
✓ Branch 6 → 7 taken 332 times.
✓ Branch 6 → 8 taken 2847 times.
3179x if (d <= duration::zero())
360 332x impl.expiry_ = (time_point::min)();
361 else
362 {
363 // Saturate rather than overflow: a clamped near-max duration
364 // (e.g. delay(hours::max())) would wrap now() + d past the
365 // clock's range and appear already elapsed.
366 2847x auto const now = clock_type::now();
367
2/2
✓ Branch 10 → 11 taken 2847 times.
✓ Branch 11 → 12 taken 2847 times.
2847x impl.expiry_ = ((time_point::max)() - now < d)
368
2/2
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 2845 times.
5692x ? (time_point::max)()
369
1/1
✓ Branch 15 → 16 taken 2845 times.
2845x : now + d;
370 }
371 3179x if (impl.heap_index_.load(std::memory_order_relaxed) ==
372
2/4
✓ Branch 32 → 33 taken 3179 times.
✗ Branch 32 → 36 not taken.
✓ Branch 37 → 38 taken 3179 times.
✗ Branch 37 → 39 not taken.
6358x implementation::npos &&
373
1/2
✓ Branch 34 → 35 taken 3179 times.
✗ Branch 34 → 36 not taken.
3179x !impl.might_have_pending_waits_.load(std::memory_order_relaxed))
374 3179x return 0;
375 return do_update_expiry();
376 }
377
378 /** Set the timer's expiry time relative to now.
379
380 This is a convenience overload that accepts any duration type
381 and converts it to the timer's native duration type. Any
382 pending asynchronous wait operations will be cancelled.
383
384 @param d The expiry time relative to now.
385
386 @return The number of pending operations that were cancelled.
387 */
388 template<class Rep, class Period>
389 std::size_t expires_after(std::chrono::duration<Rep, Period> d)
390 {
391 return expires_after(std::chrono::duration_cast<duration>(d));
392 }
393
394 /** Wait for the timer to expire.
395
396 Multiple coroutines may wait on the same timer concurrently.
397 When the timer expires, all waiters complete with success.
398
399 The operation supports cancellation via `std::stop_token` through
400 the affine awaitable protocol. If the associated stop token is
401 triggered, only that waiter completes with an error that
402 compares equal to `capy::cond::canceled`; other waiters are
403 unaffected.
404
405 This timer must outlive the returned awaitable.
406
407 @return An awaitable that completes with `io_result<>`.
408 */
409 // Defined below wait_awaitable, which needs timer complete.
410 wait_awaitable wait();
411
412 protected:
413 explicit timer(handle h) noexcept : io_object(std::move(h)) {}
414
415 private:
416 // Defined in src/corosio/src/timer.cpp, which includes both this
417 // header and timer_service.hpp, so the timer_service_* free
418 // functions are visible there.
419 std::size_t do_cancel();
420 std::size_t do_cancel_one();
421 std::size_t do_update_expiry();
422
423 /// Return the underlying implementation.
424 6364x implementation& get() const noexcept
425 {
426 6364x return *static_cast<implementation*>(h_.get());
427 }
428 };
429
430 /** Frame-resident per-wait state for a timer wait.
431
432 One node exists per `co_await` on a timer, embedded in the
433 awaitable on the suspended coroutine's frame — never allocated.
434 Once published by `implementation::wait()` the node may be
435 completed from any thread; every completion path finishes
436 touching the node before resuming or destroying the coroutine,
437 because either act may end the node's storage.
438
439 The node owns no resources: the stop token is borrowed from the
440 awaiting chain's `io_env` (which outlives the suspension) and
441 the stop callback is managed manually in `cb_buf_`, destroyed on
442 every completion path before the frame can die.
443 */
444 struct BOOST_COROSIO_SYMBOL_VISIBLE waiter_node
445 : intrusive_list<waiter_node>::node
446 {
447 // Embedded completion op — avoids heap allocation per fire/cancel.
448 // Members are exported and defined non-inline in timer.cpp: the
449 // inline waiter_node constructor references do_complete and the
450 // vtable from translation units that reach this header through
451 // delay.hpp without ever including timer_service.hpp, so the one
452 // strong definition must live in a TU that is always linked.
453 struct BOOST_COROSIO_SYMBOL_VISIBLE completion_op final : scheduler_op
454 {
455 waiter_node* waiter_ = nullptr;
456
457 BOOST_COROSIO_DECL
458 static void do_complete(
459 void* owner, scheduler_op* base, std::uint32_t, std::uint32_t);
460
461 6364x completion_op() noexcept : scheduler_op(&do_complete) {}
462
463 BOOST_COROSIO_DECL void operator()() override;
464 BOOST_COROSIO_DECL void destroy() override;
465 };
466
467 // Per-waiter stop_token cancellation
468 struct canceller
469 {
470 waiter_node* waiter_;
471 BOOST_COROSIO_DECL void operator()() const;
472 };
473
474 using stop_cb_type = std::stop_callback<canceller>;
475
476 // nullptr once removed from timer's waiter list (concurrency marker)
477 /// The timer this waiter is published on, or `nullptr`.
478 timer::implementation* impl_ = nullptr;
479
480 /// The timer service that completes this waiter.
481 timer_service* svc_ = nullptr;
482
483 /// The suspended coroutine, destroyed by the shutdown drains.
484 std::coroutine_handle<> h_;
485
486 /// The continuation posted to resume the coroutine.
487 capy::continuation cont_;
488
489 /// The executor the continuation is posted through.
490 capy::executor_ref d_;
491
492 // Borrowed from the awaiting chain's io_env, which outlives the
493 // suspension; the node holds no owning state.
494 /// The stop token observed for cancellation.
495 std::stop_token const* token_ = nullptr;
496
497 /// The completion result read by `await_resume`.
498 std::error_code ec_;
499
500 /// The embedded completion op posted to the scheduler.
501 completion_op op_;
502
503 // stop_callback is neither movable nor assignable; construct it
504 // in place once the node is pinned on the coroutine frame, and
505 // destroy it manually on every completion path.
506 /// Storage for the armed stop callback.
507 alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)];
508
509 /// True while `cb_buf_` holds a live stop callback.
510 bool cb_active_ = false;
511
512 6364x waiter_node() noexcept
513 6364x {
514 6364x op_.waiter_ = this;
515 6364x }
516
517 // The embedded op self-points and the list hooks are published
518 // to other threads; the node never moves.
519 waiter_node(waiter_node const&) = delete;
520 waiter_node& operator=(waiter_node const&) = delete;
521
522 /** Arm the stop callback.
523
524 @par Preconditions
525 `token_` is set.
526 */
527 1432x void arm_stop_cb()
528 {
529 1432x new (cb_buf_) stop_cb_type(*token_, canceller{this});
530 1432x cb_active_ = true;
531 1432x }
532
533 /// Destroy the stop callback if armed.
534 1869x void reset_stop_cb() noexcept
535 {
536
2/2
✓ Branch 2 → 3 taken 1432 times.
✓ Branch 2 → 6 taken 437 times.
1869x if (cb_active_)
537 {
538 1432x std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
539 1432x ->~stop_cb_type();
540 1432x cb_active_ = false;
541 }
542 1869x }
543 };
544
545 /** Awaitable returned by `timer::wait()`.
546
547 Carries the waiter node so a wait performs no allocation. The
548 awaitable is movable only before `await_suspend` publishes the
549 node (a move builds a fresh, quiescent node); afterwards it is
550 pinned on the coroutine frame until the wait completes.
551 */
552 struct wait_awaitable
553 {
554 timer& t_;
555 waiter_node w_;
556
557 3182x explicit wait_awaitable(timer& t) noexcept : t_(t) {}
558
559 3182x wait_awaitable(wait_awaitable&& o) noexcept : t_(o.t_) {}
560
561 wait_awaitable(wait_awaitable const&) = delete;
562 wait_awaitable& operator=(wait_awaitable const&) = delete;
563 wait_awaitable& operator=(wait_awaitable&&) = delete;
564
565 1020x bool await_ready() const noexcept
566 {
567 1020x return false;
568 }
569
570 // Cancellation surfaces through w_.ec_: the stop_token path in
571 // wait() completes the waiter with error::canceled written to
572 // it, so there is no separate token to consult here.
573 3168x capy::io_result<> await_resume() const noexcept
574 {
575 3168x return {w_.ec_};
576 }
577
578 3182x auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
579 -> std::coroutine_handle<>
580 {
581 3182x auto& impl = t_.get();
582 3182x w_.h_ = h;
583 3182x w_.cont_.h = h;
584 3182x w_.d_ = env->executor;
585
586 // Inline fast path: already expired and not in the heap
587
2/2
✓ Branch 4 → 5 taken 741 times.
✓ Branch 4 → 10 taken 2441 times.
3182x if (impl.already_expired())
588 {
589 741x w_.ec_ = {};
590 741x w_.d_.post(w_.cont_);
591 741x return std::noop_coroutine();
592 }
593
594 2441x w_.token_ = &env->stop_token;
595 2441x return impl.wait(w_);
596 }
597 };
598
599 inline wait_awaitable
600 3182x timer::wait()
601 {
602 3182x return wait_awaitable(*this);
603 }
604
605 } // namespace boost::corosio::detail
606
607 #endif
608