TLA Line data 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 : #ifndef BOOST_COROSIO_DETAIL_TIMER_HPP
12 : #define BOOST_COROSIO_DETAIL_TIMER_HPP
13 :
14 : #include <boost/corosio/detail/config.hpp>
15 : #include <boost/corosio/detail/intrusive.hpp>
16 : #include <boost/corosio/io/io_object.hpp>
17 : #include <boost/capy/continuation.hpp>
18 : #include <boost/capy/io_result.hpp>
19 : #include <boost/capy/error.hpp>
20 : #include <boost/capy/ex/executor_ref.hpp>
21 : #include <boost/capy/ex/execution_context.hpp>
22 : #include <boost/capy/ex/io_env.hpp>
23 : #include <boost/capy/concept/executor.hpp>
24 :
25 : #include <atomic>
26 : #include <chrono>
27 : #include <concepts>
28 : #include <coroutine>
29 : #include <cstddef>
30 : #include <limits>
31 : #include <stop_token>
32 : #include <system_error>
33 : #include <type_traits>
34 :
35 : namespace boost::corosio::detail {
36 :
37 : // Defined in timer_service.hpp, which includes this header. The
38 : // implementation::wait() body needs waiter_node and timer_service to
39 : // be complete, so it is declared here and defined there; intrusive_list
40 : // only stores waiter_node pointers, so the forward declaration suffices
41 : // for implementation's data layout.
42 : class timer_service;
43 : struct waiter_node;
44 :
45 : /** An asynchronous timer for coroutine I/O.
46 :
47 : This class provides asynchronous timer operations that return
48 : awaitable types. The timer can be used to schedule operations
49 : to occur after a specified duration or at a specific time point.
50 :
51 : Multiple coroutines may wait concurrently on the same timer.
52 : When the timer expires, all waiters complete with success. When
53 : the timer is cancelled, all waiters complete with an error that
54 : compares equal to `capy::cond::canceled`.
55 :
56 : Each timer operation participates in the affine awaitable protocol,
57 : ensuring coroutines resume on the correct executor.
58 :
59 : @par Thread Safety
60 : Distinct objects: Safe.@n
61 : Shared objects: Unsafe.
62 :
63 : @par Semantics
64 : Timers are not backed by per-timer kernel objects. The io_context's
65 : timer service keeps a process-side min-heap of pending expirations;
66 : the nearest expiry drives the reactor's poll timeout, and expirations
67 : are processed in the run loop.
68 : */
69 : class BOOST_COROSIO_DECL timer : public io_object
70 : {
71 : struct wait_awaitable
72 : {
73 : timer& t_;
74 : std::error_code ec_;
75 : capy::continuation cont_;
76 :
77 HIT 8457 : explicit wait_awaitable(timer& t) noexcept : t_(t) {}
78 :
79 2040 : bool await_ready() const noexcept
80 : {
81 2040 : return false;
82 : }
83 :
84 : // Cancellation surfaces through ec_: the stop_token path in
85 : // wait() completes the waiter with error::canceled written to
86 : // ec_, so there is no separate token to consult here.
87 8431 : capy::io_result<> await_resume() const noexcept
88 : {
89 8431 : return {ec_};
90 : }
91 :
92 8457 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
93 : -> std::coroutine_handle<>
94 : {
95 8457 : cont_.h = h;
96 8457 : auto& impl = t_.get();
97 : // Inline fast path: already expired and not in the heap
98 8457 : if (impl.heap_index_.load(std::memory_order_relaxed) ==
99 16914 : implementation::npos &&
100 16250 : (impl.expiry_ == (time_point::min)() ||
101 16250 : impl.expiry_ <= clock_type::now()))
102 : {
103 856 : ec_ = {};
104 856 : auto d = env->executor;
105 856 : d.post(cont_);
106 856 : return std::noop_coroutine();
107 : }
108 7601 : return impl.wait(h, env->executor, env->stop_token, &ec_, &cont_);
109 : }
110 : };
111 :
112 : public:
113 : /** Backend state and wait entry point for a timer.
114 :
115 : Holds per-timer state (expiry, heap position, waiter list) and
116 : the `wait` entry point used by the awaitable returned from
117 : @ref timer::wait. There is exactly one concrete timer backend,
118 : so `wait` is a plain member function rather than a virtual
119 : dispatch point.
120 : */
121 : struct implementation : io_object::implementation
122 : {
123 : /// Sentinel value indicating the timer is not in the heap.
124 : static constexpr std::size_t npos =
125 : (std::numeric_limits<std::size_t>::max)();
126 :
127 : // Only mutated by the owning thread (expires_at/expires_after)
128 : // before a wait is published; cross-thread consumers read the
129 : // heap entry's copied time_, never this field, so it needs no
130 : // atomicity.
131 : /// The absolute expiry time point.
132 : std::chrono::steady_clock::time_point expiry_{};
133 :
134 : // heap_index_ and might_have_pending_waits_ are cross-thread
135 : // hints, not authoritative state: the real state lives in the
136 : // heap and waiter list under timer_service::mutex_. Every
137 : // unlocked fast-out that reads them is either re-validated under
138 : // the mutex or safe under a stale value in both directions, and
139 : // any locked writer / locked reader pair is already ordered by
140 : // the mutex. All accesses therefore use memory_order_relaxed,
141 : // which keeps the lock-free fast paths fence-free while making
142 : // the concurrent reads well-defined.
143 : /// Index in the timer service's min-heap, or `npos`.
144 : std::atomic<std::size_t> heap_index_{npos};
145 :
146 : /// True if `wait()` has been called since last cancel.
147 : std::atomic<bool> might_have_pending_waits_{false};
148 :
149 : /// The timer service that owns this implementation.
150 : timer_service* svc_ = nullptr;
151 :
152 : /// Coroutines currently waiting on this timer's expiry.
153 : intrusive_list<waiter_node> waiters_;
154 :
155 : /// Free list linkage, reused when this impl is recycled.
156 : implementation* next_free_ = nullptr;
157 :
158 : /// Construct bound to the given timer service.
159 293 : explicit implementation(timer_service& svc) noexcept : svc_(&svc) {}
160 :
161 : /// Initiate an asynchronous wait for the timer to expire.
162 : // Exported at member level: dllexport on the enclosing timer
163 : // class does not extend to nested classes, and header-inline
164 : // callers (wait_awaitable::await_suspend) reference this
165 : // symbol from outside the corosio DLL.
166 : BOOST_COROSIO_DECL
167 : std::coroutine_handle<> wait(
168 : std::coroutine_handle<>,
169 : capy::executor_ref,
170 : std::stop_token,
171 : std::error_code*,
172 : capy::continuation*);
173 : };
174 :
175 : /// The clock type used for time operations.
176 : using clock_type = std::chrono::steady_clock;
177 :
178 : /// The time point type for absolute expiry times.
179 : using time_point = clock_type::time_point;
180 :
181 : /// The duration type for relative expiry times.
182 : using duration = clock_type::duration;
183 :
184 : /** Destructor.
185 :
186 : Cancels any pending operations and releases timer resources.
187 : */
188 : ~timer() override;
189 :
190 : /** Construct a timer from an execution context.
191 :
192 : @param ctx The execution context that will own this timer. It
193 : must be a corosio io_context; otherwise the constructor
194 : throws (a timer service is required).
195 :
196 : @throws std::logic_error if @p ctx is not an io_context.
197 : */
198 : explicit timer(capy::execution_context& ctx);
199 :
200 : /** Construct a timer with an initial absolute expiry time.
201 :
202 : @param ctx The execution context that will own this timer. It
203 : must be a corosio io_context; otherwise the constructor
204 : throws (a timer service is required).
205 : @param t The initial expiry time point.
206 :
207 : @throws std::logic_error if @p ctx is not an io_context.
208 : */
209 : timer(capy::execution_context& ctx, time_point t);
210 :
211 : /** Construct a timer with an initial relative expiry time.
212 :
213 : @param ctx The execution context that will own this timer. It
214 : must be a corosio io_context; otherwise the constructor
215 : throws (a timer service is required).
216 : @param d The initial expiry duration relative to now.
217 :
218 : @throws std::logic_error if @p ctx is not an io_context.
219 : */
220 : template<class Rep, class Period>
221 : timer(capy::execution_context& ctx, std::chrono::duration<Rep, Period> d)
222 : : timer(ctx)
223 : {
224 : expires_after(d);
225 : }
226 :
227 : /** Construct a timer from an executor.
228 :
229 : The timer is associated with the executor's context, which must
230 : be a corosio io_context.
231 :
232 : @param ex The executor whose context will own this timer.
233 :
234 : @throws std::logic_error if the executor's context is not an
235 : io_context.
236 : */
237 : template<class Ex>
238 : requires(!std::same_as<std::remove_cvref_t<Ex>, timer>) &&
239 : capy::Executor<Ex>
240 : explicit timer(Ex const& ex) : timer(ex.context())
241 : {
242 : }
243 :
244 : /** Construct a timer from an executor with an absolute expiry time.
245 :
246 : @param ex The executor whose context will own this timer.
247 : @param t The initial expiry time point.
248 :
249 : @throws std::logic_error if the executor's context is not an
250 : io_context.
251 : */
252 : template<class Ex>
253 : requires capy::Executor<Ex>
254 : timer(Ex const& ex, time_point t) : timer(ex.context(), t)
255 : {
256 : }
257 :
258 : /** Construct a timer from an executor with a relative expiry time.
259 :
260 : @param ex The executor whose context will own this timer.
261 : @param d The initial expiry duration relative to now.
262 :
263 : @throws std::logic_error if the executor's context is not an
264 : io_context.
265 : */
266 : template<class Ex, class Rep, class Period>
267 : requires capy::Executor<Ex>
268 : timer(Ex const& ex, std::chrono::duration<Rep, Period> d)
269 : : timer(ex.context(), d)
270 : {
271 : }
272 :
273 : /** Move constructor.
274 :
275 : Transfers ownership of the timer resources.
276 :
277 : @param other The timer to move from.
278 :
279 : @pre No awaitables returned by @p other's methods exist.
280 : @pre The execution context associated with @p other must
281 : outlive this timer.
282 : */
283 : timer(timer&& other) noexcept;
284 :
285 : /** Move assignment operator.
286 :
287 : Closes any existing timer and transfers ownership.
288 :
289 : @param other The timer to move from.
290 :
291 : @pre No awaitables returned by either `*this` or @p other's
292 : methods exist.
293 : @pre The execution context associated with @p other must
294 : outlive this timer.
295 :
296 : @return Reference to this timer.
297 : */
298 : timer& operator=(timer&& other) noexcept;
299 :
300 : timer(timer const&) = delete;
301 : timer& operator=(timer const&) = delete;
302 :
303 : /** Cancel all pending asynchronous wait operations.
304 :
305 : All outstanding operations complete with an error code that
306 : compares equal to `capy::cond::canceled`.
307 :
308 : @return The number of operations that were cancelled.
309 : */
310 : std::size_t cancel()
311 : {
312 : if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))
313 : return 0;
314 : return do_cancel();
315 : }
316 :
317 : /** Cancel one pending asynchronous wait operation.
318 :
319 : The oldest pending wait is cancelled (FIFO order). It
320 : completes with an error code that compares equal to
321 : `capy::cond::canceled`.
322 :
323 : @return The number of operations that were cancelled (0 or 1).
324 : */
325 : std::size_t cancel_one()
326 : {
327 : if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))
328 : return 0;
329 : return do_cancel_one();
330 : }
331 :
332 : /** Return the timer's expiry time as an absolute time.
333 :
334 : @return The expiry time point. If no expiry has been set,
335 : returns a default-constructed time_point.
336 : */
337 : time_point expiry() const noexcept
338 : {
339 : return get().expiry_;
340 : }
341 :
342 : /** Set the timer's expiry time as an absolute time.
343 :
344 : Any pending asynchronous wait operations will be cancelled.
345 :
346 : @param t The expiry time to be used for the timer.
347 :
348 : @return The number of pending operations that were cancelled.
349 : */
350 6 : std::size_t expires_at(time_point t)
351 : {
352 6 : auto& impl = get();
353 6 : impl.expiry_ = t;
354 6 : if (impl.heap_index_.load(std::memory_order_relaxed) ==
355 12 : implementation::npos &&
356 6 : !impl.might_have_pending_waits_.load(std::memory_order_relaxed))
357 6 : return 0;
358 MIS 0 : return do_update_expiry();
359 : }
360 :
361 : /** Set the timer's expiry time relative to now.
362 :
363 : Any pending asynchronous wait operations will be cancelled.
364 :
365 : @param d The expiry time relative to now.
366 :
367 : @return The number of pending operations that were cancelled.
368 : */
369 HIT 8451 : std::size_t expires_after(duration d)
370 : {
371 8451 : auto& impl = get();
372 8451 : if (d <= duration::zero())
373 664 : impl.expiry_ = (time_point::min)();
374 : else
375 : {
376 : // Saturate rather than overflow: a clamped near-max duration
377 : // (e.g. delay(hours::max())) would wrap now() + d past the
378 : // clock's range and appear already elapsed.
379 7787 : auto const now = clock_type::now();
380 7787 : impl.expiry_ = ((time_point::max)() - now < d)
381 15570 : ? (time_point::max)()
382 7783 : : now + d;
383 : }
384 8451 : if (impl.heap_index_.load(std::memory_order_relaxed) ==
385 16902 : implementation::npos &&
386 8451 : !impl.might_have_pending_waits_.load(std::memory_order_relaxed))
387 8451 : return 0;
388 MIS 0 : return do_update_expiry();
389 : }
390 :
391 : /** Set the timer's expiry time relative to now.
392 :
393 : This is a convenience overload that accepts any duration type
394 : and converts it to the timer's native duration type. Any
395 : pending asynchronous wait operations will be cancelled.
396 :
397 : @param d The expiry time relative to now.
398 :
399 : @return The number of pending operations that were cancelled.
400 : */
401 : template<class Rep, class Period>
402 : std::size_t expires_after(std::chrono::duration<Rep, Period> d)
403 : {
404 : return expires_after(std::chrono::duration_cast<duration>(d));
405 : }
406 :
407 : /** Wait for the timer to expire.
408 :
409 : Multiple coroutines may wait on the same timer concurrently.
410 : When the timer expires, all waiters complete with success.
411 :
412 : The operation supports cancellation via `std::stop_token` through
413 : the affine awaitable protocol. If the associated stop token is
414 : triggered, only that waiter completes with an error that
415 : compares equal to `capy::cond::canceled`; other waiters are
416 : unaffected.
417 :
418 : This timer must outlive the returned awaitable.
419 :
420 : @return An awaitable that completes with `io_result<>`.
421 : */
422 HIT 8457 : auto wait()
423 : {
424 8457 : return wait_awaitable(*this);
425 : }
426 :
427 : protected:
428 : explicit timer(handle h) noexcept : io_object(std::move(h)) {}
429 :
430 : private:
431 : // Defined in src/corosio/src/timer.cpp, which includes both this
432 : // header and timer_service.hpp, so the timer_service_* free
433 : // functions are visible there.
434 : std::size_t do_cancel();
435 : std::size_t do_cancel_one();
436 : std::size_t do_update_expiry();
437 :
438 : /// Return the underlying implementation.
439 16914 : implementation& get() const noexcept
440 : {
441 16914 : return *static_cast<implementation*>(h_.get());
442 : }
443 : };
444 :
445 : } // namespace boost::corosio::detail
446 :
447 : #endif
|