100.00% Lines (54/54)
100.00% Functions (10/10)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | + | // | ||||||
| 2 | + | // Copyright (c) 2026 Michael Vandeberg | ||||||
| 3 | + | // | ||||||
| 4 | + | // Distributed under the Boost Software License, Version 1.0. (See accompanying | ||||||
| 5 | + | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||||||
| 6 | + | // | ||||||
| 7 | + | // Official repository: https://github.com/cppalliance/capy | ||||||
| 8 | + | // | ||||||
| 9 | + | |||||||
| 10 | + | #ifndef BOOST_CAPY_DELAY_HPP | ||||||
| 11 | + | #define BOOST_CAPY_DELAY_HPP | ||||||
| 12 | + | |||||||
| 13 | + | #include <boost/capy/detail/config.hpp> | ||||||
| 14 | + | #include <boost/capy/continuation.hpp> | ||||||
| 15 | + | #include <boost/capy/error.hpp> | ||||||
| 16 | + | #include <boost/capy/ex/executor_ref.hpp> | ||||||
| 17 | + | #include <boost/capy/ex/io_env.hpp> | ||||||
| 18 | + | #include <boost/capy/ex/detail/timer_service.hpp> | ||||||
| 19 | + | #include <boost/capy/io_result.hpp> | ||||||
| 20 | + | |||||||
| 21 | + | #include <atomic> | ||||||
| 22 | + | #include <chrono> | ||||||
| 23 | + | #include <coroutine> | ||||||
| 24 | + | #include <new> | ||||||
| 25 | + | #include <stop_token> | ||||||
| 26 | + | #include <utility> | ||||||
| 27 | + | |||||||
| 28 | + | namespace boost { | ||||||
| 29 | + | namespace capy { | ||||||
| 30 | + | |||||||
| 31 | + | /** IoAwaitable returned by @ref delay. | ||||||
| 32 | + | |||||||
| 33 | + | Suspends the calling coroutine until the deadline elapses | ||||||
| 34 | + | or the environment's stop token is activated, whichever | ||||||
| 35 | + | comes first. Resumption is always posted through the | ||||||
| 36 | + | executor, never inline on the timer thread. | ||||||
| 37 | + | |||||||
| 38 | + | Not intended to be named directly; use the @ref delay | ||||||
| 39 | + | factory function instead. | ||||||
| 40 | + | |||||||
| 41 | + | @par Return Value | ||||||
| 42 | + | |||||||
| 43 | + | Returns `io_result<>{}` (no error) when the timer fires | ||||||
| 44 | + | normally, or `io_result<>{error::canceled}` when | ||||||
| 45 | + | cancellation claims the resume before the deadline. | ||||||
| 46 | + | |||||||
| 47 | + | @par Cancellation | ||||||
| 48 | + | |||||||
| 49 | + | If `stop_requested()` is true before suspension, the | ||||||
| 50 | + | coroutine resumes immediately without scheduling a timer | ||||||
| 51 | + | and returns `io_result<>{error::canceled}`. If stop is | ||||||
| 52 | + | requested while suspended, the stop callback claims the | ||||||
| 53 | + | resume and posts it through the executor; the pending | ||||||
| 54 | + | timer is cancelled on the next `await_resume` or | ||||||
| 55 | + | destructor call. | ||||||
| 56 | + | |||||||
| 57 | + | @par Thread Safety | ||||||
| 58 | + | |||||||
| 59 | + | A single `delay_awaitable` must not be awaited concurrently. | ||||||
| 60 | + | Multiple independent `delay()` calls on the same | ||||||
| 61 | + | execution_context are safe and share one timer thread. | ||||||
| 62 | + | |||||||
| 63 | + | @see delay, timeout | ||||||
| 64 | + | */ | ||||||
| 65 | + | class delay_awaitable | ||||||
| 66 | + | { | ||||||
| 67 | + | std::chrono::nanoseconds dur_; | ||||||
| 68 | + | |||||||
| 69 | + | detail::timer_service* ts_ = nullptr; | ||||||
| 70 | + | detail::timer_service::timer_id tid_ = 0; | ||||||
| 71 | + | |||||||
| 72 | + | // Declared before stop_cb_buf_: the callback | ||||||
| 73 | + | // accesses these members, so they must still be | ||||||
| 74 | + | // alive if the stop_cb_ destructor blocks. | ||||||
| 75 | + | continuation cont_; | ||||||
| 76 | + | std::atomic<bool> claimed_{false}; | ||||||
| 77 | + | bool canceled_ = false; | ||||||
| 78 | + | bool stop_cb_active_ = false; | ||||||
| 79 | + | |||||||
| 80 | + | struct cancel_fn | ||||||
| 81 | + | { | ||||||
| 82 | + | delay_awaitable* self_; | ||||||
| 83 | + | executor_ref ex_; | ||||||
| 84 | + | |||||||
| HITGNC | 85 | + | 2 | void operator()() const noexcept | ||||
| 86 | + | { | ||||||
| HITGNC | 87 | + | 2 | if(!self_->claimed_.exchange( | ||||
| 88 | + | true, std::memory_order_acq_rel)) | ||||||
| 89 | + | { | ||||||
| HITGNC | 90 | + | 2 | self_->canceled_ = true; | ||||
| HITGNC | 91 | + | 2 | ex_.post(self_->cont_); | ||||
| 92 | + | } | ||||||
| HITGNC | 93 | + | 2 | } | ||||
| 94 | + | }; | ||||||
| 95 | + | |||||||
| 96 | + | using stop_cb_t = std::stop_callback<cancel_fn>; | ||||||
| 97 | + | |||||||
| 98 | + | // Aligned storage for the stop callback. | ||||||
| 99 | + | // Declared last: its destructor may block while | ||||||
| 100 | + | // the callback accesses the members above. | ||||||
| 101 | + | BOOST_CAPY_MSVC_WARNING_PUSH | ||||||
| 102 | + | BOOST_CAPY_MSVC_WARNING_DISABLE(4324) | ||||||
| 103 | + | alignas(stop_cb_t) | ||||||
| 104 | + | unsigned char stop_cb_buf_[sizeof(stop_cb_t)]; | ||||||
| 105 | + | BOOST_CAPY_MSVC_WARNING_POP | ||||||
| 106 | + | |||||||
| HITGNC | 107 | + | 20 | stop_cb_t& stop_cb_() noexcept | ||||
| 108 | + | { | ||||||
| HITGNC | 109 | + | 20 | return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_); | ||||
| 110 | + | } | ||||||
| 111 | + | |||||||
| 112 | + | public: | ||||||
| HITGNC | 113 | + | 28 | explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept | ||||
| HITGNC | 114 | + | 28 | : dur_(dur) | ||||
| 115 | + | { | ||||||
| HITGNC | 116 | + | 28 | } | ||||
| 117 | + | |||||||
| 118 | + | /// @pre The stop callback must not be active | ||||||
| 119 | + | /// (i.e. the object has not yet been awaited). | ||||||
| HITGNC | 120 | + | 43 | delay_awaitable(delay_awaitable&& o) noexcept | ||||
| HITGNC | 121 | + | 43 | : dur_(o.dur_) | ||||
| HITGNC | 122 | + | 43 | , ts_(o.ts_) | ||||
| HITGNC | 123 | + | 43 | , tid_(o.tid_) | ||||
| HITGNC | 124 | + | 43 | , cont_(o.cont_) | ||||
| HITGNC | 125 | + | 43 | , claimed_(o.claimed_.load(std::memory_order_relaxed)) | ||||
| HITGNC | 126 | + | 43 | , canceled_(o.canceled_) | ||||
| HITGNC | 127 | + | 43 | , stop_cb_active_(std::exchange(o.stop_cb_active_, false)) | ||||
| 128 | + | { | ||||||
| HITGNC | 129 | + | 43 | } | ||||
| 130 | + | |||||||
| HITGNC | 131 | + | 71 | ~delay_awaitable() | ||||
| 132 | + | { | ||||||
| HITGNC | 133 | + | 71 | if(stop_cb_active_) | ||||
| HITGNC | 134 | + | 2 | stop_cb_().~stop_cb_t(); | ||||
| HITGNC | 135 | + | 71 | if(ts_) | ||||
| HITGNC | 136 | + | 20 | ts_->cancel(tid_); | ||||
| HITGNC | 137 | + | 71 | } | ||||
| 138 | + | |||||||
| 139 | + | delay_awaitable(delay_awaitable const&) = delete; | ||||||
| 140 | + | delay_awaitable& operator=(delay_awaitable const&) = delete; | ||||||
| 141 | + | delay_awaitable& operator=(delay_awaitable&&) = delete; | ||||||
| 142 | + | |||||||
| HITGNC | 143 | + | 27 | bool await_ready() const noexcept | ||||
| 144 | + | { | ||||||
| HITGNC | 145 | + | 27 | return dur_.count() <= 0; | ||||
| 146 | + | } | ||||||
| 147 | + | |||||||
| 148 | + | std::coroutine_handle<> | ||||||
| HITGNC | 149 | + | 25 | await_suspend( | ||||
| 150 | + | std::coroutine_handle<> h, | ||||||
| 151 | + | io_env const* env) noexcept | ||||||
| 152 | + | { | ||||||
| 153 | + | // Already stopped: resume immediately | ||||||
| HITGNC | 154 | + | 25 | if(env->stop_token.stop_requested()) | ||||
| 155 | + | { | ||||||
| HITGNC | 156 | + | 5 | canceled_ = true; | ||||
| HITGNC | 157 | + | 5 | return h; | ||||
| 158 | + | } | ||||||
| 159 | + | |||||||
| HITGNC | 160 | + | 20 | cont_.h = h; | ||||
| HITGNC | 161 | + | 20 | ts_ = &env->executor.context().use_service<detail::timer_service>(); | ||||
| 162 | + | |||||||
| 163 | + | // Schedule timer (won't fire inline since deadline is in the future) | ||||||
| HITGNC | 164 | + | 20 | tid_ = ts_->schedule_after(dur_, | ||||
| HITGNC | 165 | + | 20 | [this, ex = env->executor]() | ||||
| 166 | + | { | ||||||
| HITGNC | 167 | + | 17 | if(!claimed_.exchange( | ||||
| 168 | + | true, std::memory_order_acq_rel)) | ||||||
| 169 | + | { | ||||||
| HITGNC | 170 | + | 17 | ex.post(cont_); | ||||
| 171 | + | } | ||||||
| HITGNC | 172 | + | 17 | }); | ||||
| 173 | + | |||||||
| 174 | + | // Register stop callback (may fire inline) | ||||||
| HITGNC | 175 | + | 60 | ::new(stop_cb_buf_) stop_cb_t( | ||||
| HITGNC | 176 | + | 20 | env->stop_token, | ||||
| HITGNC | 177 | + | 20 | cancel_fn{this, env->executor}); | ||||
| HITGNC | 178 | + | 20 | stop_cb_active_ = true; | ||||
| 179 | + | |||||||
| HITGNC | 180 | + | 20 | return std::noop_coroutine(); | ||||
| 181 | + | } | ||||||
| 182 | + | |||||||
| HITGNC | 183 | + | 26 | io_result<> await_resume() noexcept | ||||
| 184 | + | { | ||||||
| HITGNC | 185 | + | 26 | if(stop_cb_active_) | ||||
| 186 | + | { | ||||||
| HITGNC | 187 | + | 18 | stop_cb_().~stop_cb_t(); | ||||
| HITGNC | 188 | + | 18 | stop_cb_active_ = false; | ||||
| 189 | + | } | ||||||
| HITGNC | 190 | + | 26 | if(ts_) | ||||
| HITGNC | 191 | + | 18 | ts_->cancel(tid_); | ||||
| HITGNC | 192 | + | 26 | if(canceled_) | ||||
| HITGNC | 193 | + | 6 | return io_result<>{make_error_code(error::canceled)}; | ||||
| HITGNC | 194 | + | 20 | return io_result<>{}; | ||||
| 195 | + | } | ||||||
| 196 | + | }; | ||||||
| 197 | + | |||||||
| 198 | + | /** Suspend the current coroutine for a duration. | ||||||
| 199 | + | |||||||
| 200 | + | Returns an IoAwaitable that completes at or after the | ||||||
| 201 | + | specified duration, or earlier if the environment's stop | ||||||
| 202 | + | token is activated. | ||||||
| 203 | + | |||||||
| 204 | + | Zero or negative durations complete synchronously without | ||||||
| 205 | + | scheduling a timer. | ||||||
| 206 | + | |||||||
| 207 | + | @par Example | ||||||
| 208 | + | @code | ||||||
| 209 | + | auto [ec] = co_await delay(std::chrono::milliseconds(100)); | ||||||
| 210 | + | @endcode | ||||||
| 211 | + | |||||||
| 212 | + | @param dur The duration to wait. | ||||||
| 213 | + | |||||||
| 214 | + | @return A @ref delay_awaitable whose `await_resume` | ||||||
| 215 | + | returns `io_result<>`. On normal completion, `ec` | ||||||
| 216 | + | is clear. On cancellation, `ec == error::canceled`. | ||||||
| 217 | + | |||||||
| 218 | + | @throws Nothing. | ||||||
| 219 | + | |||||||
| 220 | + | @see timeout, delay_awaitable | ||||||
| 221 | + | */ | ||||||
| 222 | + | template<typename Rep, typename Period> | ||||||
| 223 | + | delay_awaitable | ||||||
| HITGNC | 224 | + | 27 | delay(std::chrono::duration<Rep, Period> dur) noexcept | ||||
| 225 | + | { | ||||||
| 226 | + | return delay_awaitable{ | ||||||
| HITGNC | 227 | + | 27 | std::chrono::duration_cast<std::chrono::nanoseconds>(dur)}; | ||||
| 228 | + | } | ||||||
| 229 | + | |||||||
| 230 | + | } // capy | ||||||
| 231 | + | } // boost | ||||||
| 232 | + | |||||||
| 233 | + | #endif | ||||||