src/corosio/src/timer.cpp

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