src/corosio/src/timer.cpp

87.3% Lines (48/0/55) 90.0% List of functions (9/0/10)
timer.cpp
f(x) Functions (10)
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 #include <boost/corosio/detail/timer.hpp>
12 #include <boost/corosio/detail/timer_service.hpp>
13
14 namespace boost::corosio::detail {
15
16 10742x timer::~timer() = default;
17
18 10746x timer::timer(capy::execution_context& ctx)
19 10746x : io_object(create_handle<detail::timer_service>(ctx))
20 {
21 10742x }
22
23 // Not inline: wait_awaitable::await_suspend (defined in timer.hpp) calls
24 // this from translation units that may never include timer_service.hpp,
25 // so this must be the one strong definition the linker can always find
26 // wherever a detail::timer is used (every such user also needs timer's
27 // constructors, defined in this same translation unit).
28 std::coroutine_handle<>
29 9876x timer::implementation::wait(waiter_node& w)
30 {
31 // Already-expired fast path — no publication, no mutex.
32 // Post instead of dispatch so the coroutine yields to the
33 // scheduler, allowing other queued work to run.
34 9876x if (already_expired())
35 {
36 1x w.ec_ = {};
37 1x w.d_.post(w.cont_);
38 1x return std::noop_coroutine();
39 }
40 9875x return publish(w);
41 }
42
43 std::coroutine_handle<>
44 9889x timer::implementation::publish(waiter_node& w)
45 {
46 // Publication-last invariant: fully initialize the waiter, count
47 // its work, and arm cancellation BEFORE insert_waiter() publishes
48 // it into the heap/waiter slot where a concurrent run() thread can fire
49 // it. impl_ stays null until insert_waiter() sets it under the
50 // mutex, so a stop callback that fires early (cancel_waiter) sees a
51 // null impl_ and is a safe no-op. To avoid losing such an early
52 // cancel, insert_waiter() re-checks stop_requested() under the lock
53 // and completes as canceled if it fires in this window.
54 9889x w.impl_ = nullptr;
55 9889x w.svc_ = svc_;
56
57 9889x might_have_pending_waits_.store(true, std::memory_order_relaxed);
58 9889x svc_->get_scheduler().work_started();
59
60 9889x if (w.token_->stop_possible())
61 1436x w.arm_stop_cb();
62
63 9889x svc_->insert_waiter(*this, &w);
64
65 9889x return std::noop_coroutine();
66 }
67
68 std::coroutine_handle<>
69 14x timer::publish_wait(waiter_node& w)
70 {
71 14x return get().publish(w);
72 }
73
74 bool
75 12x timer::rearm_wait(waiter_node& w, duration d) noexcept
76 {
77 // The single waiter was popped before its op ran, so the impl is
78 // out of the heap with no published waiters: expires_after only
79 // stores the saturated expiry, and writing it is race-free.
80 12x expires_after(d);
81 12x auto& impl = get();
82 // The drain that popped the waiter cleared the flag.
83 12x impl.might_have_pending_waits_.store(true, std::memory_order_relaxed);
84 try
85 {
86 12x impl.svc_->insert_waiter(impl, &w);
87 }
88 catch(std::bad_alloc const&)
89 {
90 // insert_waiter grows the heap before publishing anything,
91 // so the waiter is untouched and the caller can complete
92 // the wait through the normal resume path.
93 return false;
94 }
95 12x return true;
96 }
97
98 // completion_op and canceller definitions live here, non-inline, for
99 // the same reason wait() does: the inline waiter_node constructor in
100 // timer.hpp references do_complete and the vtable from translation
101 // units that never include timer_service.hpp.
102
103 void
104 1400x waiter_node::canceller::operator()() const
105 {
106 1400x waiter_->svc_->cancel_waiter(waiter_);
107 1400x }
108
109 void
110 waiter_node::completion_op::do_complete(
111 [[maybe_unused]] void* owner,
112 scheduler_op* base,
113 std::uint32_t,
114 std::uint32_t)
115 {
116 // owner is always non-null here. The destroy path (owner == nullptr)
117 // is unreachable because completion_op overrides destroy() directly,
118 // bypassing scheduler_op::destroy() which would call func_(nullptr, ...).
119 BOOST_COROSIO_ASSERT(owner);
120 static_cast<completion_op*>(base)->operator()();
121 }
122
123 void
124 9871x waiter_node::completion_op::operator()()
125 {
126 // The node lives in the resuming coroutine's frame: posting the
127 // continuation is the last access, since the frame (and node)
128 // may complete and die on another thread immediately after.
129 9871x auto* w = waiter_;
130 // A true return means the waiter re-published itself: the frame
131 // stays suspended, the wait's work count stays live, and the
132 // node may already be firing on another thread — no access past
133 // this point.
134 9871x if (w->on_fire_ && w->on_fire_(w->on_fire_ctx_))
135 12x return;
136 9859x w->reset_stop_cb();
137 9859x auto d = w->d_;
138 9859x auto& sched = w->svc_->get_scheduler();
139 9859x d.post(w->cont_);
140 9859x sched.work_finished();
141 }
142
143 void
144 2x waiter_node::completion_op::destroy()
145 {
146 // Called during scheduler shutdown drain when this completion_op is
147 // in the scheduler's ready queue (posted by cancel_timer() or
148 // process_expired()). Balances the work_started() from
149 // implementation::wait(), keeping the run-loop counter sane; no
150 // shutdown path waits on that counter.
151 //
152 // This override also prevents scheduler_op::destroy() from calling
153 // do_complete(nullptr, ...). See also: timer_service::shutdown()
154 // which drains waiters still in the timer heap (the other path).
155 // Destroying the frame also ends the node's storage, so it is
156 // the last access.
157 2x auto* w = waiter_;
158 2x w->reset_stop_cb();
159 2x auto h = std::exchange(w->h_, {});
160 2x auto& sched = w->svc_->get_scheduler();
161 2x sched.work_finished();
162 2x if (h)
163 2x h.destroy();
164 2x }
165
166 } // namespace boost::corosio::detail
167