src/corosio/src/timer.cpp

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