100.00% Lines (60/60) 100.00% Functions (16/16)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Steve Gerbino 3   // Copyright (c) 2026 Steve Gerbino
4   // 4   //
5   // Distributed under the Boost Software License, Version 1.0. (See accompanying 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) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7   // 7   //
8   // Official repository: https://github.com/cppalliance/corosio 8   // Official repository: https://github.com/cppalliance/corosio
9   // 9   //
10   10  
11   #ifndef BOOST_COROSIO_DETAIL_TIMER_HPP 11   #ifndef BOOST_COROSIO_DETAIL_TIMER_HPP
12   #define BOOST_COROSIO_DETAIL_TIMER_HPP 12   #define BOOST_COROSIO_DETAIL_TIMER_HPP
13   13  
14   #include <boost/corosio/detail/config.hpp> 14   #include <boost/corosio/detail/config.hpp>
15   #include <boost/corosio/detail/intrusive.hpp> 15   #include <boost/corosio/detail/intrusive.hpp>
16   #include <boost/corosio/detail/scheduler_op.hpp> 16   #include <boost/corosio/detail/scheduler_op.hpp>
17   #include <boost/corosio/io/io_object.hpp> 17   #include <boost/corosio/io/io_object.hpp>
18   #include <boost/capy/continuation.hpp> 18   #include <boost/capy/continuation.hpp>
19   #include <boost/capy/io_result.hpp> 19   #include <boost/capy/io_result.hpp>
20   #include <boost/capy/error.hpp> 20   #include <boost/capy/error.hpp>
21   #include <boost/capy/ex/executor_ref.hpp> 21   #include <boost/capy/ex/executor_ref.hpp>
22   #include <boost/capy/ex/execution_context.hpp> 22   #include <boost/capy/ex/execution_context.hpp>
23   #include <boost/capy/ex/io_env.hpp> 23   #include <boost/capy/ex/io_env.hpp>
24   #include <boost/capy/concept/executor.hpp> 24   #include <boost/capy/concept/executor.hpp>
25   25  
26   #include <atomic> 26   #include <atomic>
27   #include <chrono> 27   #include <chrono>
28   #include <concepts> 28   #include <concepts>
29   #include <coroutine> 29   #include <coroutine>
30   #include <cstddef> 30   #include <cstddef>
31   #include <limits> 31   #include <limits>
32   #include <new> 32   #include <new>
33   #include <stop_token> 33   #include <stop_token>
34   #include <system_error> 34   #include <system_error>
35   #include <type_traits> 35   #include <type_traits>
36   36  
37   namespace boost::corosio::detail { 37   namespace boost::corosio::detail {
38   38  
39   // timer_service is defined in timer_service.hpp, which includes this 39   // timer_service is defined in timer_service.hpp, which includes this
40   // header. waiter_node and wait_awaitable are defined below the timer 40   // header. waiter_node and wait_awaitable are defined below the timer
41   // class: waiter_node stores a timer::implementation*, which cannot be 41   // class: waiter_node stores a timer::implementation*, which cannot be
42 - // forward-declared as a nested type. intrusive_list only stores 42 + // forward-declared as a nested type. implementation stores only a
43 - // waiter_node pointers, so this forward declaration suffices for 43 + // waiter_node pointer, so this forward declaration suffices for its
44 - // implementation's data layout. 44 + // data layout.
45   class timer_service; 45   class timer_service;
46   struct waiter_node; 46   struct waiter_node;
47   struct wait_awaitable; 47   struct wait_awaitable;
48   48  
49   /** An asynchronous timer for coroutine I/O. 49   /** An asynchronous timer for coroutine I/O.
50   50  
51   This class provides asynchronous timer operations that return 51   This class provides asynchronous timer operations that return
52   awaitable types. The timer can be used to schedule operations 52   awaitable types. The timer can be used to schedule operations
53   to occur after a specified duration or at a specific time point. 53   to occur after a specified duration or at a specific time point.
54   54  
55 - Multiple coroutines may wait concurrently on the same timer. 55 + Each timer carries at most one wait: `delay` and `timeout` own a
56 - When the timer expires, all waiters complete with success. When 56 + private timer per `co_await`. When the timer expires the waiter
57 - the timer is cancelled, all waiters complete with an error that 57 + completes with success; a cancelled wait completes with an error
58 - compares equal to `capy::cond::canceled`. 58 + that compares equal to `capy::cond::canceled`.
59   59  
60   Each timer operation participates in the affine awaitable protocol, 60   Each timer operation participates in the affine awaitable protocol,
61   ensuring coroutines resume on the correct executor. 61   ensuring coroutines resume on the correct executor.
62   62  
63   @par Thread Safety 63   @par Thread Safety
64   Distinct objects: Safe.@n 64   Distinct objects: Safe.@n
65   Shared objects: Unsafe. 65   Shared objects: Unsafe.
66   66  
67   @par Semantics 67   @par Semantics
68   Timers are not backed by per-timer kernel objects. The io_context's 68   Timers are not backed by per-timer kernel objects. The io_context's
69   timer service keeps a process-side min-heap of pending expirations; 69   timer service keeps a process-side min-heap of pending expirations;
70   the nearest expiry drives the reactor's poll timeout, and expirations 70   the nearest expiry drives the reactor's poll timeout, and expirations
71   are processed in the run loop. 71   are processed in the run loop.
72   */ 72   */
73   class BOOST_COROSIO_DECL timer : public io_object 73   class BOOST_COROSIO_DECL timer : public io_object
74   { 74   {
75   friend struct wait_awaitable; 75   friend struct wait_awaitable;
76   76  
77   public: 77   public:
78   /** Backend state and wait entry point for a timer. 78   /** Backend state and wait entry point for a timer.
79   79  
80 - Holds per-timer state (expiry, heap position, waiter list) and 80 + Holds per-timer state ( expiry, heap position, the single waiter ) and
81   the `wait` entry point used by the awaitable returned from 81   the `wait` entry point used by the awaitable returned from
82   @ref timer::wait. There is exactly one concrete timer backend, 82   @ref timer::wait. There is exactly one concrete timer backend,
83   so `wait` is a plain member function rather than a virtual 83   so `wait` is a plain member function rather than a virtual
84   dispatch point. 84   dispatch point.
85   */ 85   */
86   struct implementation : io_object::implementation 86   struct implementation : io_object::implementation
87   { 87   {
88   /// Sentinel value indicating the timer is not in the heap. 88   /// Sentinel value indicating the timer is not in the heap.
89   static constexpr std::size_t npos = 89   static constexpr std::size_t npos =
90   (std::numeric_limits<std::size_t>::max)(); 90   (std::numeric_limits<std::size_t>::max)();
91   91  
92   // Only mutated by the owning thread (expires_at/expires_after) 92   // Only mutated by the owning thread (expires_at/expires_after)
93   // before a wait is published; cross-thread consumers read the 93   // before a wait is published; cross-thread consumers read the
94   // heap entry's copied time_, never this field, so it needs no 94   // heap entry's copied time_, never this field, so it needs no
95   // atomicity. 95   // atomicity.
96   /// The absolute expiry time point. 96   /// The absolute expiry time point.
97   std::chrono::steady_clock::time_point expiry_{}; 97   std::chrono::steady_clock::time_point expiry_{};
98   98  
99   // heap_index_ and might_have_pending_waits_ are cross-thread 99   // heap_index_ and might_have_pending_waits_ are cross-thread
100   // hints, not authoritative state: the real state lives in the 100   // hints, not authoritative state: the real state lives in the
101 - // heap and waiter list under timer_service::mutex_. Every 101 + // heap and the published waiter under timer_service::mutex_. Every
102   // unlocked fast-out that reads them is either re-validated under 102   // unlocked fast-out that reads them is either re-validated under
103   // the mutex or safe under a stale value in both directions, and 103   // the mutex or safe under a stale value in both directions, and
104   // any locked writer / locked reader pair is already ordered by 104   // any locked writer / locked reader pair is already ordered by
105   // the mutex. All accesses therefore use memory_order_relaxed, 105   // the mutex. All accesses therefore use memory_order_relaxed,
106   // which keeps the lock-free fast paths fence-free while making 106   // which keeps the lock-free fast paths fence-free while making
107   // the concurrent reads well-defined. 107   // the concurrent reads well-defined.
108   /// Index in the timer service's min-heap, or `npos`. 108   /// Index in the timer service's min-heap, or `npos`.
109   std::atomic<std::size_t> heap_index_{npos}; 109   std::atomic<std::size_t> heap_index_{npos};
110   110  
  111 + // false implies waiter_ is null: both are cleared together
  112 + // under the service mutex.
111   /// True if `wait()` has been called since last cancel. 113   /// True if `wait()` has been called since last cancel.
112   std::atomic<bool> might_have_pending_waits_{false}; 114   std::atomic<bool> might_have_pending_waits_{false};
113   115  
114   /// The timer service that owns this implementation. 116   /// The timer service that owns this implementation.
115   timer_service* svc_ = nullptr; 117   timer_service* svc_ = nullptr;
116   118  
117 - /// Coroutines currently waiting on this timer's expiry. 119 + // Exactly one wait may be outstanding: delay and timeout own
118 - intrusive_list<waiter_node> waiters_; 120 + // a private timer per co_await, and the service's drains rely
  121 + // on the one-to-one pairing.
  122 + /// The waiter published on this timer, or `nullptr`.
  123 + waiter_node* waiter_ = nullptr;
119   124  
120   /// Free list linkage, reused when this impl is recycled. 125   /// Free list linkage, reused when this impl is recycled.
121   implementation* next_free_ = nullptr; 126   implementation* next_free_ = nullptr;
122   127  
123   /// Construct bound to the given timer service. 128   /// Construct bound to the given timer service.
HITCBC 124   310 explicit implementation(timer_service& svc) noexcept : svc_(&svc) {} 129   317 explicit implementation(timer_service& svc) noexcept : svc_(&svc) {}
125   130  
126   /** Check whether the timer is expired and absent from the heap. 131   /** Check whether the timer is expired and absent from the heap.
127   132  
128   The single definition of the already-expired fast-path 133   The single definition of the already-expired fast-path
129   predicate: `await_suspend` tests it inline and `wait()` 134   predicate: `await_suspend` tests it inline and `wait()`
130   re-tests it because the expiry can elapse between the two 135   re-tests it because the expiry can elapse between the two
131   reads. 136   reads.
132   */ 137   */
HITCBC 133   12981 bool already_expired() const noexcept 138   20820 bool already_expired() const noexcept
134   { 139   {
HITCBC 135   38943 return heap_index_.load(std::memory_order_relaxed) == npos && 140   62460 return heap_index_.load(std::memory_order_relaxed) == npos &&
HITCBC 136   12981 (expiry_ == 141   20820 (expiry_ ==
HITCBC 137   25298 (std::chrono::steady_clock::time_point::min)() || 142   40976 (std::chrono::steady_clock::time_point::min)() ||
HITCBC 138   25298 expiry_ <= std::chrono::steady_clock::now()); 143   40976 expiry_ <= std::chrono::steady_clock::now());
139   } 144   }
140   145  
141   /** Asynchronously wait for the timer to expire. 146   /** Asynchronously wait for the timer to expire.
142   147  
143 - Publishes the waiter into the service's heap and waiter 148 + Publishes the waiter into the service's heap and the
144 - list, after which it may complete on any thread. If the 149 + timer's waiter slot, after which it may complete on any
145 - timer is already expired and not in the heap, completes 150 + thread. If the timer is already expired and not in the
146 - by posting the continuation without publishing. 151 + heap, completes by posting the continuation without
  152 + publishing.
147   153  
148   @par Preconditions 154   @par Preconditions
149   @p w is fully initialized, and its storage (the awaitable 155   @p w is fully initialized, and its storage (the awaitable
150   on the suspended coroutine's frame) outlives the wait. 156   on the suspended coroutine's frame) outlives the wait.
151   157  
152   @param w The waiter to publish. 158   @param w The waiter to publish.
153   */ 159   */
154   // Exported at member level: dllexport on the enclosing timer 160   // Exported at member level: dllexport on the enclosing timer
155   // class does not extend to nested classes, and header-inline 161   // class does not extend to nested classes, and header-inline
156   // callers (wait_awaitable::await_suspend) reference this 162   // callers (wait_awaitable::await_suspend) reference this
157   // symbol from outside the corosio DLL. 163   // symbol from outside the corosio DLL.
158   BOOST_COROSIO_DECL 164   BOOST_COROSIO_DECL
159   std::coroutine_handle<> wait(waiter_node& w); 165   std::coroutine_handle<> wait(waiter_node& w);
  166 +
  167 + /** Publish a waiter unconditionally.
  168 +
  169 + Like `wait`, but never takes the elapsed fast path. The
  170 + fast path posts the continuation directly, bypassing the
  171 + embedded op; hook-driven waits must observe every
  172 + completion through the op, where the re-arm hook runs.
  173 +
  174 + @par Preconditions
  175 + Same as `wait`.
  176 +
  177 + @param w The waiter to publish.
  178 + */
  179 + std::coroutine_handle<> publish(waiter_node& w);
160   }; 180   };
161   181  
162   /// The clock type used for time operations. 182   /// The clock type used for time operations.
163   using clock_type = std::chrono::steady_clock; 183   using clock_type = std::chrono::steady_clock;
164   184  
165   /// The time point type for absolute expiry times. 185   /// The time point type for absolute expiry times.
166   using time_point = clock_type::time_point; 186   using time_point = clock_type::time_point;
167   187  
168   /// The duration type for relative expiry times. 188   /// The duration type for relative expiry times.
169   using duration = clock_type::duration; 189   using duration = clock_type::duration;
170   190  
171   /** Destructor. 191   /** Destructor.
172   192  
173   Cancels any pending operations and releases timer resources. 193   Cancels any pending operations and releases timer resources.
174   */ 194   */
175   ~timer() override; 195   ~timer() override;
176   196  
177   /** Construct a timer from an execution context. 197   /** Construct a timer from an execution context.
178   198  
179   @param ctx The execution context that will own this timer. It 199   @param ctx The execution context that will own this timer. It
180   must be a corosio io_context; otherwise the constructor 200   must be a corosio io_context; otherwise the constructor
181   throws (a timer service is required). 201   throws (a timer service is required).
182   202  
183   @throws std::logic_error if @p ctx is not an io_context. 203   @throws std::logic_error if @p ctx is not an io_context.
184   */ 204   */
185   explicit timer(capy::execution_context& ctx); 205   explicit timer(capy::execution_context& ctx);
186   206  
187   /** Construct a timer with an initial absolute expiry time. 207   /** Construct a timer with an initial absolute expiry time.
188   208  
189   @param ctx The execution context that will own this timer. It 209   @param ctx The execution context that will own this timer. It
190   must be a corosio io_context; otherwise the constructor 210   must be a corosio io_context; otherwise the constructor
191   throws (a timer service is required). 211   throws (a timer service is required).
192   @param t The initial expiry time point. 212   @param t The initial expiry time point.
193   213  
194   @throws std::logic_error if @p ctx is not an io_context. 214   @throws std::logic_error if @p ctx is not an io_context.
195   */ 215   */
196   timer(capy::execution_context& ctx, time_point t); 216   timer(capy::execution_context& ctx, time_point t);
197   217  
198   /** Construct a timer with an initial relative expiry time. 218   /** Construct a timer with an initial relative expiry time.
199   219  
200   @param ctx The execution context that will own this timer. It 220   @param ctx The execution context that will own this timer. It
201   must be a corosio io_context; otherwise the constructor 221   must be a corosio io_context; otherwise the constructor
202   throws (a timer service is required). 222   throws (a timer service is required).
203   @param d The initial expiry duration relative to now. 223   @param d The initial expiry duration relative to now.
204   224  
205   @throws std::logic_error if @p ctx is not an io_context. 225   @throws std::logic_error if @p ctx is not an io_context.
206   */ 226   */
207   template<class Rep, class Period> 227   template<class Rep, class Period>
208   timer(capy::execution_context& ctx, std::chrono::duration<Rep, Period> d) 228   timer(capy::execution_context& ctx, std::chrono::duration<Rep, Period> d)
209   : timer(ctx) 229   : timer(ctx)
210   { 230   {
211   expires_after(d); 231   expires_after(d);
212   } 232   }
213   233  
214   /** Construct a timer from an executor. 234   /** Construct a timer from an executor.
215   235  
216   The timer is associated with the executor's context, which must 236   The timer is associated with the executor's context, which must
217   be a corosio io_context. 237   be a corosio io_context.
218   238  
219   @param ex The executor whose context will own this timer. 239   @param ex The executor whose context will own this timer.
220   240  
221   @throws std::logic_error if the executor's context is not an 241   @throws std::logic_error if the executor's context is not an
222   io_context. 242   io_context.
223   */ 243   */
224   template<class Ex> 244   template<class Ex>
225   requires(!std::same_as<std::remove_cvref_t<Ex>, timer>) && 245   requires(!std::same_as<std::remove_cvref_t<Ex>, timer>) &&
226   capy::Executor<Ex> 246   capy::Executor<Ex>
227   explicit timer(Ex const& ex) : timer(ex.context()) 247   explicit timer(Ex const& ex) : timer(ex.context())
228   { 248   {
229   } 249   }
230   250  
231   /** Construct a timer from an executor with an absolute expiry time. 251   /** Construct a timer from an executor with an absolute expiry time.
232   252  
233   @param ex The executor whose context will own this timer. 253   @param ex The executor whose context will own this timer.
234   @param t The initial expiry time point. 254   @param t The initial expiry time point.
235   255  
236   @throws std::logic_error if the executor's context is not an 256   @throws std::logic_error if the executor's context is not an
237   io_context. 257   io_context.
238   */ 258   */
239   template<class Ex> 259   template<class Ex>
240   requires capy::Executor<Ex> 260   requires capy::Executor<Ex>
241   timer(Ex const& ex, time_point t) : timer(ex.context(), t) 261   timer(Ex const& ex, time_point t) : timer(ex.context(), t)
242   { 262   {
243   } 263   }
244   264  
245   /** Construct a timer from an executor with a relative expiry time. 265   /** Construct a timer from an executor with a relative expiry time.
246   266  
247   @param ex The executor whose context will own this timer. 267   @param ex The executor whose context will own this timer.
248   @param d The initial expiry duration relative to now. 268   @param d The initial expiry duration relative to now.
249   269  
250   @throws std::logic_error if the executor's context is not an 270   @throws std::logic_error if the executor's context is not an
251   io_context. 271   io_context.
252   */ 272   */
253   template<class Ex, class Rep, class Period> 273   template<class Ex, class Rep, class Period>
254   requires capy::Executor<Ex> 274   requires capy::Executor<Ex>
255   timer(Ex const& ex, std::chrono::duration<Rep, Period> d) 275   timer(Ex const& ex, std::chrono::duration<Rep, Period> d)
256   : timer(ex.context(), d) 276   : timer(ex.context(), d)
257   { 277   {
258   } 278   }
259   279  
260   /** Move constructor. 280   /** Move constructor.
261   281  
262   Transfers ownership of the timer resources. 282   Transfers ownership of the timer resources.
263   283  
264   @param other The timer to move from. 284   @param other The timer to move from.
265   285  
266   @pre No awaitables returned by @p other's methods exist. 286   @pre No awaitables returned by @p other's methods exist.
267   @pre The execution context associated with @p other must 287   @pre The execution context associated with @p other must
268   outlive this timer. 288   outlive this timer.
269   */ 289   */
270   timer(timer&& other) noexcept; 290   timer(timer&& other) noexcept;
271   291  
272   /** Move assignment operator. 292   /** Move assignment operator.
273   293  
274   Closes any existing timer and transfers ownership. 294   Closes any existing timer and transfers ownership.
275   295  
276   @param other The timer to move from. 296   @param other The timer to move from.
277   297  
278   @pre No awaitables returned by either `*this` or @p other's 298   @pre No awaitables returned by either `*this` or @p other's
279   methods exist. 299   methods exist.
280   @pre The execution context associated with @p other must 300   @pre The execution context associated with @p other must
281   outlive this timer. 301   outlive this timer.
282   302  
283   @return Reference to this timer. 303   @return Reference to this timer.
284   */ 304   */
285   timer& operator=(timer&& other) noexcept; 305   timer& operator=(timer&& other) noexcept;
286   306  
287   timer(timer const&) = delete; 307   timer(timer const&) = delete;
288   timer& operator=(timer const&) = delete; 308   timer& operator=(timer const&) = delete;
289 - /** Cancel all pending asynchronous wait operations.  
290 -  
291 - All outstanding operations complete with an error code that  
292 - compares equal to `capy::cond::canceled`.  
293 -  
294 - @return The number of operations that were cancelled.  
295 - */  
296 - std::size_t cancel()  
297 - {  
298 - if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))  
299 - return 0;  
300 - return do_cancel();  
301 - }  
302 -  
303 - /** Cancel one pending asynchronous wait operation.  
304 -  
305 - The oldest pending wait is cancelled (FIFO order). It  
306 - completes with an error code that compares equal to  
307 - `capy::cond::canceled`.  
308 -  
309 - @return The number of operations that were cancelled (0 or 1).  
310 - */  
311 - std::size_t cancel_one()  
312 - {  
313 - if (!get().might_have_pending_waits_.load(std::memory_order_relaxed))  
314 - return 0;  
315 - return do_cancel_one();  
316 - }  
317 -  
318   309  
319   /** Return the timer's expiry time as an absolute time. 310   /** Return the timer's expiry time as an absolute time.
320   311  
321   @return The expiry time point. If no expiry has been set, 312   @return The expiry time point. If no expiry has been set,
322   returns a default-constructed time_point. 313   returns a default-constructed time_point.
323   */ 314   */
324   time_point expiry() const noexcept 315   time_point expiry() const noexcept
325   { 316   {
326   return get().expiry_; 317   return get().expiry_;
327   } 318   }
328   319  
329   /** Set the timer's expiry time as an absolute time. 320   /** Set the timer's expiry time as an absolute time.
330   321  
331 - Any pending asynchronous wait operations will be cancelled. 322 + @par Preconditions
  323 + No wait is published on this timer.
332   324  
333 -  
334 - @return The number of pending operations that were cancelled.  
335   @param t The expiry time to be used for the timer. 325   @param t The expiry time to be used for the timer.
336   */ 326   */
HITCBC 337 - 16 std::size_t expires_at(time_point t) 327 + 16 void expires_at(time_point t)
338   { 328   {
HITCBC 339 - 16 auto& impl = get(); 329 + 16 auto& impl = get();
HITGNC   330 + 32 BOOST_COROSIO_ASSERT(
  331 + impl.heap_index_.load(std::memory_order_relaxed) ==
  332 + implementation::npos);
DCB 340 - 16 if (impl.heap_index_.load(std::memory_order_relaxed) ==  
DCB 341 - 16 implementation::npos &&  
DCB 342 - 32 !impl.might_have_pending_waits_.load(std::memory_order_relaxed))  
DCB 343 - 16 return 0;  
DCB 344 - 16 return do_update_expiry();  
HITGBC 345   impl.expiry_ = t; 333   16 impl.expiry_ = t;
HITGIC 346   } 334   16 }
347   335  
348   /** Set the timer's expiry time relative to now. 336   /** Set the timer's expiry time relative to now.
349   337  
350 - Any pending asynchronous wait operations will be cancelled. 338 + @par Preconditions
  339 + No wait is published on this timer.
351   340  
352 -  
353 - @return The number of pending operations that were cancelled.  
354   @param d The expiry time relative to now. 341   @param d The expiry time relative to now.
355   */ 342   */
HITCBC 356 - 6898 std::size_t expires_after(duration d) 343 + 10849 void expires_after(duration d)
357   { 344   {
HITCBC 358   6898 auto& impl = get(); 345   10849 auto& impl = get();
HITGNC   346 + 21698 BOOST_COROSIO_ASSERT(
  347 + impl.heap_index_.load(std::memory_order_relaxed) ==
  348 + implementation::npos);
HITCBC 359   6898 if (d <= duration::zero()) 349   10849 if (d <= duration::zero())
HITCBC 360   664 impl.expiry_ = (time_point::min)(); 350   680 impl.expiry_ = (time_point::min)();
361   else 351   else
362   { 352   {
363   // Saturate rather than overflow: a clamped near-max duration 353   // Saturate rather than overflow: a clamped near-max duration
364   // (e.g. delay(hours::max())) would wrap now() + d past the 354   // (e.g. delay(hours::max())) would wrap now() + d past the
365   // clock's range and appear already elapsed. 355   // clock's range and appear already elapsed.
HITCBC 366   6234 auto const now = clock_type::now(); 356   10169 auto const now = clock_type::now();
HITCBC 367   6234 impl.expiry_ = ((time_point::max)() - now < d) 357   10169 impl.expiry_ = ((time_point::max)() - now < d)
HITCBC 368   12464 ? (time_point::max)() 358   20334 ? (time_point::max)()
HITCBC 369   6230 : now + d; 359   10165 : now + d;
370 - if (impl.heap_index_.load(std::memory_order_relaxed) ==  
DCB 371 - 6898 implementation::npos &&  
DCB 372 - 13796 !impl.might_have_pending_waits_.load(std::memory_order_relaxed))  
DCB 373 - 6898 return 0;  
DCB 374 - 6898 return do_update_expiry();  
EUB 375   } 360   }
HITGIC 376   } 361   10849 }
377   362  
378   /** Set the timer's expiry time relative to now. 363   /** Set the timer's expiry time relative to now.
379   364  
380   This is a convenience overload that accepts any duration type 365   This is a convenience overload that accepts any duration type
381 - and converts it to the timer's native duration type. Any 366 + and converts it to the timer's native duration type.
382 - pending asynchronous wait operations will be cancelled.  
383   367  
384 -  
385 - @return The number of pending operations that were cancelled.  
386   @param d The expiry time relative to now. 368   @param d The expiry time relative to now.
387   */ 369   */
388   template<class Rep, class Period> 370   template<class Rep, class Period>
389 - std::size_t expires_after(std::chrono::duration<Rep, Period> d) 371 + void expires_after(std::chrono::duration<Rep, Period> d)
390   { 372   {
391 - return expires_after(std::chrono::duration_cast<duration>(d)); 373 + expires_after(std::chrono::duration_cast<duration>(d));
392   } 374   }
393   375  
394   /** Wait for the timer to expire. 376   /** Wait for the timer to expire.
395   377  
396 - Multiple coroutines may wait on the same timer concurrently. 378 + At most one wait may be outstanding at a time.
397 - When the timer expires, all waiters complete with success.  
398   379  
399   The operation supports cancellation via `std::stop_token` through 380   The operation supports cancellation via `std::stop_token` through
400   the affine awaitable protocol. If the associated stop token is 381   the affine awaitable protocol. If the associated stop token is
401   triggered, only that waiter completes with an error that 382   triggered, only that waiter completes with an error that
402 - compares equal to `capy::cond::canceled`; other waiters are 383 + compares equal to `capy::cond::canceled`.
403 - unaffected.  
404   384  
405   This timer must outlive the returned awaitable. 385   This timer must outlive the returned awaitable.
406   386  
407   @return An awaitable that completes with `io_result<>`. 387   @return An awaitable that completes with `io_result<>`.
408   */ 388   */
409   // Defined below wait_awaitable, which needs timer complete. 389   // Defined below wait_awaitable, which needs timer complete.
410   wait_awaitable wait(); 390   wait_awaitable wait();
411   391  
  392 + /** Publish a hook-driven wait.
  393 +
  394 + Bypasses the elapsed fast path so every completion is
  395 + delivered through the waiter's embedded op, where the
  396 + re-arm hook is consulted. Used by awaitables that
  397 + re-publish the waiter to continue a logical wait across
  398 + several timer expirations.
  399 +
  400 + @par Preconditions
  401 + @p w is fully initialized ( handle, executor, stop token,
  402 + hook fields ) and its storage outlives the wait.
  403 +
  404 + @param w The waiter to publish.
  405 +
  406 + @return `std::noop_coroutine()`.
  407 + */
  408 + std::coroutine_handle<> publish_wait(waiter_node& w);
  409 +
  410 + /** Re-arm an already-fired waiter with a new relative expiry.
  411 +
  412 + Stores the ( saturated ) expiry and re-publishes @p w. The
  413 + waiter's original work count and stop callback remain in
  414 + effect. Must only be called from the waiter's re-arm hook,
  415 + where the waiter has been popped from the service but not
  416 + yet resumed.
  417 +
  418 + @par Preconditions
  419 + The timer has no other waiters — this is what makes the
  420 + unlocked expiry write race-free.
  421 +
  422 + Re-publication needs heap capacity and can fail under
  423 + allocation pressure. On failure the waiter is left exactly as
  424 + the hook received it, so the caller completes the wait through
  425 + the normal resume path instead of re-arming.
  426 +
  427 + @param w The waiter to re-publish.
  428 + @param d The next expiry relative to now.
  429 +
  430 + @return `true` if re-published; `false` if allocation failed.
  431 + */
  432 + [[nodiscard]] bool rearm_wait(waiter_node& w, duration d) noexcept;
  433 +
412   protected: 434   protected:
413   explicit timer(handle h) noexcept : io_object(std::move(h)) {} 435   explicit timer(handle h) noexcept : io_object(std::move(h)) {}
414   436  
415 - // Defined in src/corosio/src/timer.cpp, which includes both this  
416 - // header and timer_service.hpp, so the timer_service_* free  
417 - // functions are visible there.  
418 - std::size_t do_cancel();  
419 - std::size_t do_cancel_one();  
420 - std::size_t do_update_expiry();  
421 -  
422   private: 437   private:
423   /// Return the underlying implementation. 438   /// Return the underlying implementation.
HITCBC 424   13828 implementation& get() const noexcept 439   21730 implementation& get() const noexcept
425   { 440   {
HITCBC 426   13828 return *static_cast<implementation*>(h_.get()); 441   21730 return *static_cast<implementation*>(h_.get());
427   } 442   }
428   }; 443   };
429   444  
430   /** Frame-resident per-wait state for a timer wait. 445   /** Frame-resident per-wait state for a timer wait.
431   446  
432   One node exists per `co_await` on a timer, embedded in the 447   One node exists per `co_await` on a timer, embedded in the
433   awaitable on the suspended coroutine's frame — never allocated. 448   awaitable on the suspended coroutine's frame — never allocated.
434   Once published by `implementation::wait()` the node may be 449   Once published by `implementation::wait()` the node may be
435   completed from any thread; every completion path finishes 450   completed from any thread; every completion path finishes
436   touching the node before resuming or destroying the coroutine, 451   touching the node before resuming or destroying the coroutine,
437   because either act may end the node's storage. 452   because either act may end the node's storage.
438   453  
439   The node owns no resources: the stop token is borrowed from the 454   The node owns no resources: the stop token is borrowed from the
440   awaiting chain's `io_env` (which outlives the suspension) and 455   awaiting chain's `io_env` (which outlives the suspension) and
441   the stop callback is managed manually in `cb_buf_`, destroyed on 456   the stop callback is managed manually in `cb_buf_`, destroyed on
442   every completion path before the frame can die. 457   every completion path before the frame can die.
443   */ 458   */
444   struct BOOST_COROSIO_SYMBOL_VISIBLE waiter_node 459   struct BOOST_COROSIO_SYMBOL_VISIBLE waiter_node
445   : intrusive_list<waiter_node>::node 460   : intrusive_list<waiter_node>::node
446   { 461   {
447   // Embedded completion op — avoids heap allocation per fire/cancel. 462   // Embedded completion op — avoids heap allocation per fire/cancel.
448   // Members are exported and defined non-inline in timer.cpp: the 463   // Members are exported and defined non-inline in timer.cpp: the
449   // inline waiter_node constructor references do_complete and the 464   // inline waiter_node constructor references do_complete and the
450   // vtable from translation units that reach this header through 465   // vtable from translation units that reach this header through
451   // delay.hpp without ever including timer_service.hpp, so the one 466   // delay.hpp without ever including timer_service.hpp, so the one
452   // strong definition must live in a TU that is always linked. 467   // strong definition must live in a TU that is always linked.
453   struct BOOST_COROSIO_SYMBOL_VISIBLE completion_op final : scheduler_op 468   struct BOOST_COROSIO_SYMBOL_VISIBLE completion_op final : scheduler_op
454   { 469   {
455   waiter_node* waiter_ = nullptr; 470   waiter_node* waiter_ = nullptr;
456   471  
457   BOOST_COROSIO_DECL 472   BOOST_COROSIO_DECL
458   static void do_complete( 473   static void do_complete(
459   void* owner, scheduler_op* base, std::uint32_t, std::uint32_t); 474   void* owner, scheduler_op* base, std::uint32_t, std::uint32_t);
460   475  
HITCBC 461   13828 completion_op() noexcept : scheduler_op(&do_complete) {} 476   23718 completion_op() noexcept : scheduler_op(&do_complete) {}
462   477  
463   BOOST_COROSIO_DECL void operator()() override; 478   BOOST_COROSIO_DECL void operator()() override;
464   BOOST_COROSIO_DECL void destroy() override; 479   BOOST_COROSIO_DECL void destroy() override;
465   }; 480   };
466   481  
467   // Per-waiter stop_token cancellation 482   // Per-waiter stop_token cancellation
468   struct canceller 483   struct canceller
469   { 484   {
470   waiter_node* waiter_; 485   waiter_node* waiter_;
471   BOOST_COROSIO_DECL void operator()() const; 486   BOOST_COROSIO_DECL void operator()() const;
472   }; 487   };
473   488  
474   using stop_cb_type = std::stop_callback<canceller>; 489   using stop_cb_type = std::stop_callback<canceller>;
475   490  
476 - // nullptr once removed from timer's waiter list (concurrency marker) 491 + // nullptr once unpublished from the timer ( concurrency marker )
477   /// The timer this waiter is published on, or `nullptr`. 492   /// The timer this waiter is published on, or `nullptr`.
478   timer::implementation* impl_ = nullptr; 493   timer::implementation* impl_ = nullptr;
479   494  
480   /// The timer service that completes this waiter. 495   /// The timer service that completes this waiter.
481   timer_service* svc_ = nullptr; 496   timer_service* svc_ = nullptr;
482   497  
483   /// The suspended coroutine, destroyed by the shutdown drains. 498   /// The suspended coroutine, destroyed by the shutdown drains.
484   std::coroutine_handle<> h_; 499   std::coroutine_handle<> h_;
485   500  
486   /// The continuation posted to resume the coroutine. 501   /// The continuation posted to resume the coroutine.
487   capy::continuation cont_; 502   capy::continuation cont_;
488   503  
489   /// The executor the continuation is posted through. 504   /// The executor the continuation is posted through.
490   capy::executor_ref d_; 505   capy::executor_ref d_;
491   506  
492   // Borrowed from the awaiting chain's io_env, which outlives the 507   // Borrowed from the awaiting chain's io_env, which outlives the
493   // suspension; the node holds no owning state. 508   // suspension; the node holds no owning state.
494   /// The stop token observed for cancellation. 509   /// The stop token observed for cancellation.
495   std::stop_token const* token_ = nullptr; 510   std::stop_token const* token_ = nullptr;
496   511  
497   /// The completion result read by `await_resume`. 512   /// The completion result read by `await_resume`.
498   std::error_code ec_; 513   std::error_code ec_;
499   514  
  515 + // Consulted by the completion op before resuming; lets a
  516 + // clock-facade wait re-publish itself instead of completing.
  517 + // Never consulted on the shutdown destroy path. Consulted on
  518 + // every completion, including cancellation ( `ec_` set ) — the
  519 + // hook must inspect `w`'s `ec_` and must not re-arm a canceled
  520 + // waiter. Runs inside the completion path; must not throw.
  521 + /// Re-arm hook: return true to skip resumption ( wait continues ).
  522 + bool (*on_fire_)(void*) noexcept = nullptr;
  523 +
  524 + /// Context passed to `on_fire_` ( the owning awaitable ).
  525 + void* on_fire_ctx_ = nullptr;
  526 +
500   /// The embedded completion op posted to the scheduler. 527   /// The embedded completion op posted to the scheduler.
501   completion_op op_; 528   completion_op op_;
502   529  
503   // stop_callback is neither movable nor assignable; construct it 530   // stop_callback is neither movable nor assignable; construct it
504   // in place once the node is pinned on the coroutine frame, and 531   // in place once the node is pinned on the coroutine frame, and
505   // destroy it manually on every completion path. 532   // destroy it manually on every completion path.
506   /// Storage for the armed stop callback. 533   /// Storage for the armed stop callback.
507   alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)]; 534   alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)];
508   535  
509   /// True while `cb_buf_` holds a live stop callback. 536   /// True while `cb_buf_` holds a live stop callback.
510   bool cb_active_ = false; 537   bool cb_active_ = false;
511   538  
HITCBC 512   13828 waiter_node() noexcept 539   23718 waiter_node() noexcept
HITCBC 513   13828 { 540   23718 {
HITCBC 514   13828 op_.waiter_ = this; 541   23718 op_.waiter_ = this;
HITCBC 515   13828 } 542   23718 }
516   543  
517   // The embedded op self-points and the list hooks are published 544   // The embedded op self-points and the list hooks are published
518   // to other threads; the node never moves. 545   // to other threads; the node never moves.
519   waiter_node(waiter_node const&) = delete; 546   waiter_node(waiter_node const&) = delete;
520   waiter_node& operator=(waiter_node const&) = delete; 547   waiter_node& operator=(waiter_node const&) = delete;
521   548  
  549 + /** Bind the coroutine and its environment before publication.
  550 +
  551 + The single definition of the fields every wait must populate
  552 + before the node is published; hook-driven waits additionally
  553 + set `on_fire_` / `on_fire_ctx_`.
  554 +
  555 + @param h The coroutine to resume on completion.
  556 + @param env The awaiting chain's environment; must outlive
  557 + the suspension.
  558 + */
HITGNC   559 + 10853 void bind(std::coroutine_handle<> h, capy::io_env const& env) noexcept
  560 + {
HITGNC   561 + 10853 h_ = h;
HITGNC   562 + 10853 cont_.h = h;
HITGNC   563 + 10853 d_ = env.executor;
HITGNC   564 + 10853 token_ = &env.stop_token;
HITGNC   565 + 10853 }
  566 +
522   /** Arm the stop callback. 567   /** Arm the stop callback.
523   568  
524   @par Preconditions 569   @par Preconditions
525   `token_` is set. 570   `token_` is set.
526   */ 571   */
HITCBC 527   1434 void arm_stop_cb() 572   1431 void arm_stop_cb()
528   { 573   {
HITCBC 529   1434 new (cb_buf_) stop_cb_type(*token_, canceller{this}); 574   1431 new (cb_buf_) stop_cb_type(*token_, canceller{this});
HITCBC 530   1434 cb_active_ = true; 575   1431 cb_active_ = true;
HITCBC 531   1434 } 576   1431 }
532   577  
533   /// Destroy the stop callback if armed. 578   /// Destroy the stop callback if armed.
HITCBC 534   6067 void reset_stop_cb() noexcept 579   9986 void reset_stop_cb() noexcept
535   { 580   {
HITCBC 536   6067 if (cb_active_) 581   9986 if (cb_active_)
537   { 582   {
HITCBC 538   1434 std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_)) 583   1431 std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
HITCBC 539   1434 ->~stop_cb_type(); 584   1431 ->~stop_cb_type();
HITCBC 540   1434 cb_active_ = false; 585   1431 cb_active_ = false;
541   } 586   }
HITCBC 542   6067 } 587   9986 }
543   }; 588   };
544   589  
545   /** Awaitable returned by `timer::wait()`. 590   /** Awaitable returned by `timer::wait()`.
546   591  
547   Carries the waiter node so a wait performs no allocation. The 592   Carries the waiter node so a wait performs no allocation. The
548   awaitable is movable only before `await_suspend` publishes the 593   awaitable is movable only before `await_suspend` publishes the
549   node (a move builds a fresh, quiescent node); afterwards it is 594   node (a move builds a fresh, quiescent node); afterwards it is
550   pinned on the coroutine frame until the wait completes. 595   pinned on the coroutine frame until the wait completes.
551   */ 596   */
552   struct wait_awaitable 597   struct wait_awaitable
553   { 598   {
554   timer& t_; 599   timer& t_;
555   waiter_node w_; 600   waiter_node w_;
556   601  
HITCBC 557   6914 explicit wait_awaitable(timer& t) noexcept : t_(t) {} 602   10843 explicit wait_awaitable(timer& t) noexcept : t_(t) {}
558   603  
HITCBC 559   6914 wait_awaitable(wait_awaitable&& o) noexcept : t_(o.t_) {} 604   10843 wait_awaitable(wait_awaitable&& o) noexcept : t_(o.t_) {}
560   605  
561   wait_awaitable(wait_awaitable const&) = delete; 606   wait_awaitable(wait_awaitable const&) = delete;
562   wait_awaitable& operator=(wait_awaitable const&) = delete; 607   wait_awaitable& operator=(wait_awaitable const&) = delete;
563   wait_awaitable& operator=(wait_awaitable&&) = delete; 608   wait_awaitable& operator=(wait_awaitable&&) = delete;
564   609  
HITCBC 565   2053 bool await_ready() const noexcept 610   2053 bool await_ready() const noexcept
566   { 611   {
HITCBC 567   2053 return false; 612   2053 return false;
568   } 613   }
569   614  
570   // Cancellation surfaces through w_.ec_: the stop_token path in 615   // Cancellation surfaces through w_.ec_: the stop_token path in
571   // wait() completes the waiter with error::canceled written to 616   // wait() completes the waiter with error::canceled written to
572   // it, so there is no separate token to consult here. 617   // it, so there is no separate token to consult here.
HITCBC 573   6886 capy::io_result<> await_resume() const noexcept 618   10815 capy::io_result<> await_resume() const noexcept
574   { 619   {
HITCBC 575   6886 return {w_.ec_}; 620   10815 return {w_.ec_};
576   } 621   }
577   622  
HITCBC 578   6914 auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) 623   10843 auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
579   -> std::coroutine_handle<> 624   -> std::coroutine_handle<>
580   { 625   {
HITCBC 581   6914 auto& impl = t_.get(); 626   10843 auto& impl = t_.get();
HITCBC 582 - 6914 w_.h_ = h; 627 + 10843 w_.bind(h, *env);
DCB 583 - 6914 w_.cont_.h = h;  
DCB 584 - 6914 w_.d_ = env->executor;  
585   628  
586 - // Inline fast path: already expired and not in the heap 629 + // Inline fast path: already expired and not in the heap.
  630 + // Post instead of dispatch so the coroutine yields to the
  631 + // scheduler, allowing other queued work to run.
HITCBC 587   6914 if (impl.already_expired()) 632   10843 if (impl.already_expired())
588   { 633   {
HITCBC 589   847 w_.ec_ = {}; 634   866 w_.ec_ = {};
HITCBC 590   847 w_.d_.post(w_.cont_); 635   866 w_.d_.post(w_.cont_);
HITCBC 591   847 return std::noop_coroutine(); 636   866 return std::noop_coroutine();
592   } 637   }
593 - w_.token_ = &env->stop_token;  
ECB 594   6067 638  
HITCBC 595   6067 return impl.wait(w_); 639   9977 return impl.wait(w_);
596   } 640   }
597   }; 641   };
598   642  
599   inline wait_awaitable 643   inline wait_awaitable
HITCBC 600   6914 timer::wait() 644   10843 timer::wait()
601   { 645   {
HITCBC 602   6914 return wait_awaitable(*this); 646   10843 return wait_awaitable(*this);
603   } 647   }
604   648  
605   } // namespace boost::corosio::detail 649   } // namespace boost::corosio::detail
606   650  
607   #endif 651   #endif