src/corosio/src/timer.cpp

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