src/corosio/src/timer.cpp

84.1% Lines (58 / 69) 58.3% Branches (14 / 24) 91.7% Functions (11 / 12)
timer.cpp
f(x) Functions (12)
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 #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 30666x 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 15329x attached_timer_service(capy::execution_context& ctx)
27 {
28 15329x auto& svc = ctx.use_service<timer_service>();
29 15329x svc.attach();
30 15329x return svc;
31 }
32
33 } // namespace
34
35 30661x timer::timer(capy::execution_context& ctx)
36 15329x : io_object(handle(ctx, attached_timer_service(ctx)))
37 30661x {
38 30661x }
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 1102x timer::implementation::retire() noexcept
45 {
46
1/2
✓ Branch 0 taken 1102 times.
✗ Branch 1 not taken.
1102x svc_->pool_.recycle(this);
47 1102x }
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 14412x 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
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 14391 times.
14412x if (already_expired())
61 {
62 21x w.ec_ = {};
63 21x w.d_.post(w.cont_);
64 21x return std::noop_coroutine();
65 }
66 14391x return publish(w);
67 14412x }
68
69 std::coroutine_handle<>
70 14609x 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 14609x w.impl_ = nullptr;
81 14609x w.svc_ = svc_;
82
83 14609x might_have_pending_waits_.store(true, std::memory_order_relaxed);
84 14609x svc_->get_scheduler().work_started();
85
86
2/2
✓ Branch 0 taken 8637 times.
✓ Branch 1 taken 5972 times.
14609x if (w.token_->stop_possible())
87 5972x 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
1/2
✓ Branch 0 taken 14609 times.
✗ Branch 1 not taken.
14609x svc_->insert_waiter(*this, &w);
99 14609x }
100 catch (...)
101 {
102 ✗ w.reset_stop_cb();
103 ✗ svc_->get_scheduler().work_finished();
104
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ throw;
105 ✗ }
106
107 14609x return std::noop_coroutine();
108 ✗ }
109
110 std::coroutine_handle<>
111 216x timer::publish_wait(waiter_node& w)
112 {
113 216x return get().publish(w);
114 }
115
116 bool
117 1076x 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
1/2
✓ Branch 0 taken 1076 times.
✗ Branch 1 not taken.
1076x expires_after(d);
123 1076x auto& impl = get();
124 // The drain that popped the waiter cleared the flag.
125 1076x impl.might_have_pending_waits_.store(true, std::memory_order_relaxed);
126 try
127 {
128
1/2
✓ Branch 0 taken 1076 times.
✗ Branch 1 not taken.
1076x impl.svc_->insert_waiter(impl, &w);
129 1076x }
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 1076x return true;
138 1076x }
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 3041x waiter_node::canceller::operator()() const
147 {
148 3041x waiter_->svc_->cancel_waiter(waiter_);
149 3041x }
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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ BOOST_COROSIO_ASSERT(owner);
162 ✗ static_cast<completion_op*>(base)->operator()();
163 ✗ }
164
165 void
166 12431x 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 12431x 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
4/4
✓ Branch 0 taken 1290 times.
✓ Branch 1 taken 11141 times.
✓ Branch 2 taken 214 times.
✓ Branch 3 taken 1076 times.
12431x if (w->on_fire_ && w->on_fire_(w->on_fire_ctx_))
177 1076x return;
178 11355x w->reset_stop_cb();
179 11355x auto d = w->d_;
180 11355x auto& sched = w->svc_->get_scheduler();
181
1/2
✓ Branch 0 taken 11355 times.
✗ Branch 1 not taken.
11355x dispatch_coro(d, w->cont_).resume();
182 11355x sched.work_finished();
183 12431x }
184
185 void
186 18x 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 18x auto* w = waiter_;
200 18x w->reset_stop_cb();
201 18x auto c = std::exchange(w->cont_, {});
202 auto& sched = w->svc_->get_scheduler();
203 sched.work_finished();
204
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18x if (c.h)
205 18x capy::abandon(c);
206 }
207
208 } // namespace boost::corosio::detail
209