93.33% Lines (42/45)
100.00% Functions (8/8)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | // | 1 | // | |||||
| 2 | // Copyright (c) 2026 Steve Gerbino | 2 | // Copyright (c) 2026 Steve Gerbino | |||||
| 3 | // | 3 | // | |||||
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | 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) | 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | |||||
| 6 | // | 6 | // | |||||
| 7 | // Official repository: https://github.com/cppalliance/corosio | 7 | // Official repository: https://github.com/cppalliance/corosio | |||||
| 8 | // | 8 | // | |||||
| 9 | 9 | |||||||
| 10 | #ifndef BOOST_COROSIO_DELAY_HPP | 10 | #ifndef BOOST_COROSIO_DELAY_HPP | |||||
| 11 | #define BOOST_COROSIO_DELAY_HPP | 11 | #define BOOST_COROSIO_DELAY_HPP | |||||
| 12 | 12 | |||||||
| 13 | #include <boost/corosio/detail/config.hpp> | 13 | #include <boost/corosio/detail/config.hpp> | |||||
| 14 | #include <boost/corosio/detail/except.hpp> | 14 | #include <boost/corosio/detail/except.hpp> | |||||
| 15 | #include <boost/corosio/detail/timer.hpp> | 15 | #include <boost/corosio/detail/timer.hpp> | |||||
| 16 | #include <boost/capy/error.hpp> | 16 | #include <boost/capy/error.hpp> | |||||
| 17 | #include <boost/capy/ex/io_env.hpp> | 17 | #include <boost/capy/ex/io_env.hpp> | |||||
| 18 | #include <boost/capy/io_result.hpp> | 18 | #include <boost/capy/io_result.hpp> | |||||
| 19 | 19 | |||||||
| 20 | #include <chrono> | 20 | #include <chrono> | |||||
| 21 | #include <coroutine> | 21 | #include <coroutine> | |||||
| 22 | #include <exception> | 22 | #include <exception> | |||||
| 23 | #include <optional> | 23 | #include <optional> | |||||
| 24 | #include <stdexcept> | 24 | #include <stdexcept> | |||||
| 25 | 25 | |||||||
| 26 | namespace boost::corosio { | 26 | namespace boost::corosio { | |||||
| 27 | 27 | |||||||
| 28 | /** IoAwaitable returned by @ref delay. | 28 | /** IoAwaitable returned by @ref delay. | |||||
| 29 | 29 | |||||||
| 30 | Suspends the calling coroutine until the deadline elapses or | 30 | Suspends the calling coroutine until the deadline elapses or | |||||
| 31 | the environment's stop token is activated, whichever comes | 31 | the environment's stop token is activated, whichever comes | |||||
| 32 | first. A deadline already elapsed at suspension, or a stop | 32 | first. A deadline already elapsed at suspension, or a stop | |||||
| 33 | token already active, resumes the coroutine inline, without | 33 | token already active, resumes the coroutine inline, without | |||||
| 34 | starting a timer (see Cancellation below). Otherwise the | 34 | starting a timer (see Cancellation below). Otherwise the | |||||
| 35 | coroutine resumes through the executor once the timer fires | 35 | coroutine resumes through the executor once the timer fires | |||||
| 36 | or a mid-wait cancellation arrives. | 36 | or a mid-wait cancellation arrives. | |||||
| 37 | 37 | |||||||
| 38 | Not intended to be named directly; use the @ref delay factory | 38 | Not intended to be named directly; use the @ref delay factory | |||||
| 39 | overloads instead. | 39 | overloads instead. | |||||
| 40 | 40 | |||||||
| 41 | @par Preconditions | 41 | @par Preconditions | |||||
| 42 | The awaiting coroutine's executor must belong to an | 42 | The awaiting coroutine's executor must belong to an | |||||
| 43 | `io_context`. Any other execution context terminates with a | 43 | `io_context`. Any other execution context terminates with a | |||||
| 44 | diagnostic, because silently running without a timer would | 44 | diagnostic, because silently running without a timer would | |||||
| 45 | drop the requested delay. | 45 | drop the requested delay. | |||||
| 46 | 46 | |||||||
| 47 | @par Cancellation | 47 | @par Cancellation | |||||
| 48 | If stop is already requested before suspension, the coroutine | 48 | If stop is already requested before suspension, the coroutine | |||||
| 49 | resumes immediately with `error::canceled`. If stop is | 49 | resumes immediately with `error::canceled`. If stop is | |||||
| 50 | requested while suspended, the pending wait is cancelled and | 50 | requested while suspended, the pending wait is cancelled and | |||||
| 51 | the coroutine resumes with `error::canceled`. Requesting stop | 51 | the coroutine resumes with `error::canceled`. Requesting stop | |||||
| 52 | from another thread while the io_context runs in | 52 | from another thread while the io_context runs in | |||||
| 53 | single_threaded mode (auto-enabled at concurrency_hint == 1) | 53 | single_threaded mode (auto-enabled at concurrency_hint == 1) | |||||
| 54 | is not permitted by io_context's threading rules; | 54 | is not permitted by io_context's threading rules; | |||||
| 55 | cross-thread cancellation requires a multi-threaded-capable | 55 | cross-thread cancellation requires a multi-threaded-capable | |||||
| 56 | context. | 56 | context. | |||||
| 57 | 57 | |||||||
| 58 | @see delay | 58 | @see delay | |||||
| 59 | */ | 59 | */ | |||||
| 60 | class delay_awaitable | 60 | class delay_awaitable | |||||
| 61 | { | 61 | { | |||||
| 62 | // wait() names timer's private awaitable type; decltype is | 62 | // wait() names timer's private awaitable type; decltype is | |||||
| 63 | // the only way to store it here. | 63 | // the only way to store it here. | |||||
| 64 | using wait_type = decltype(std::declval<detail::timer&>().wait()); | 64 | using wait_type = decltype(std::declval<detail::timer&>().wait()); | |||||
| 65 | 65 | |||||||
| 66 | std::chrono::steady_clock::time_point deadline_{}; | 66 | std::chrono::steady_clock::time_point deadline_{}; | |||||
| 67 | std::chrono::nanoseconds dur_{}; | 67 | std::chrono::nanoseconds dur_{}; | |||||
| 68 | bool has_deadline_ = false; | 68 | bool has_deadline_ = false; | |||||
| 69 | bool canceled_ = false; | 69 | bool canceled_ = false; | |||||
| 70 | std::optional<detail::timer> timer_; | 70 | std::optional<detail::timer> timer_; | |||||
| 71 | std::optional<wait_type> wait_; | 71 | std::optional<wait_type> wait_; | |||||
| 72 | 72 | |||||||
| 73 | public: | 73 | public: | |||||
| 74 | /// Construct an awaitable that waits for `dur` nanoseconds. | 74 | /// Construct an awaitable that waits for `dur` nanoseconds. | |||||
| HITCBC | 75 | 11382 | explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept | 75 | 10424 | explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept | ||
| HITCBC | 76 | 11382 | : dur_(dur) | 76 | 10424 | : dur_(dur) | ||
| 77 | { | 77 | { | |||||
| HITCBC | 78 | 11382 | } | 78 | 10424 | } | ||
| 79 | 79 | |||||||
| 80 | /// Construct an awaitable that waits until `tp`. | 80 | /// Construct an awaitable that waits until `tp`. | |||||
| HITCBC | 81 | 6 | explicit delay_awaitable( | 81 | 6 | explicit delay_awaitable( | ||
| 82 | std::chrono::steady_clock::time_point tp) noexcept | 82 | std::chrono::steady_clock::time_point tp) noexcept | |||||
| HITCBC | 83 | 6 | : deadline_(tp) | 83 | 6 | : deadline_(tp) | ||
| HITCBC | 84 | 6 | , has_deadline_(true) | 84 | 6 | , has_deadline_(true) | ||
| 85 | { | 85 | { | |||||
| HITCBC | 86 | 6 | } | 86 | 6 | } | ||
| 87 | 87 | |||||||
| 88 | /// Construct by transferring state from `other`. | 88 | /// Construct by transferring state from `other`. | |||||
| 89 | // Only moved before await_suspend; wait_ is engaged after. | 89 | // Only moved before await_suspend; wait_ is engaged after. | |||||
| HITCBC | 90 | 13416 | delay_awaitable(delay_awaitable&&) = default; | 90 | 12458 | delay_awaitable(delay_awaitable&&) = default; | ||
| 91 | 91 | |||||||
| 92 | delay_awaitable(delay_awaitable const&) = delete; | 92 | delay_awaitable(delay_awaitable const&) = delete; | |||||
| 93 | delay_awaitable& operator=(delay_awaitable const&) = delete; | 93 | delay_awaitable& operator=(delay_awaitable const&) = delete; | |||||
| 94 | delay_awaitable& operator=(delay_awaitable&&) = delete; | 94 | delay_awaitable& operator=(delay_awaitable&&) = delete; | |||||
| 95 | 95 | |||||||
| 96 | /// Return false unconditionally; see await_suspend. | 96 | /// Return false unconditionally; see await_suspend. | |||||
| 97 | // The elapsed-deadline fast path must run after the stop-token | 97 | // The elapsed-deadline fast path must run after the stop-token | |||||
| 98 | // check, and only await_suspend receives the env carrying it. | 98 | // check, and only await_suspend receives the env carrying it. | |||||
| HITCBC | 99 | 9358 | bool await_ready() const noexcept | 99 | 8400 | bool await_ready() const noexcept | ||
| 100 | { | 100 | { | |||||
| HITCBC | 101 | 9358 | return false; | 101 | 8400 | return false; | ||
| 102 | } | 102 | } | |||||
| 103 | 103 | |||||||
| 104 | /// Resume inline if stopped or elapsed; else wait on a timer. | 104 | /// Resume inline if stopped or elapsed; else wait on a timer. | |||||
| 105 | std::coroutine_handle<> | 105 | std::coroutine_handle<> | |||||
| HITCBC | 106 | 11388 | await_suspend(std::coroutine_handle<> h, capy::io_env const* env) | 106 | 10430 | await_suspend(std::coroutine_handle<> h, capy::io_env const* env) | ||
| 107 | { | 107 | { | |||||
| HITCBC | 108 | 11388 | if(env->stop_token.stop_requested()) | 108 | 10430 | if(env->stop_token.stop_requested()) | ||
| 109 | { | 109 | { | |||||
| HITCBC | 110 | 3947 | canceled_ = true; | 110 | 4003 | canceled_ = true; | ||
| HITCBC | 111 | 3947 | return h; | 111 | 4003 | return h; | ||
| 112 | } | 112 | } | |||||
| 113 | 113 | |||||||
| 114 | // Elapsed deadlines complete synchronously, but only once a | 114 | // Elapsed deadlines complete synchronously, but only once a | |||||
| 115 | // pending stop request has already been ruled out above. | 115 | // pending stop request has already been ruled out above. | |||||
| HITCBC | 116 | 14878 | if(has_deadline_ ? | 116 | 12850 | if(has_deadline_ ? | ||
| HITCBC | 117 | 7441 | deadline_ <= std::chrono::steady_clock::now() : | 117 | 6427 | deadline_ <= std::chrono::steady_clock::now() : | ||
| HITCBC | 118 | 7437 | dur_.count() <= 0) | 118 | 6423 | dur_.count() <= 0) | ||
| HITCBC | 119 | 27 | return h; | 119 | 8 | return h; | ||
| 120 | 120 | |||||||
| 121 | // A non-io_context executor cannot supply a timer service, | 121 | // A non-io_context executor cannot supply a timer service, | |||||
| 122 | // and await_suspend is driven through a noexcept wrapper, so | 122 | // and await_suspend is driven through a noexcept wrapper, so | |||||
| 123 | // translate the service-lookup failure into a clear terminate. | 123 | // translate the service-lookup failure into a clear terminate. | |||||
| 124 | try | 124 | try | |||||
| 125 | { | 125 | { | |||||
| HITCBC | 126 | 7414 | timer_.emplace(env->executor.context()); | 126 | 6419 | timer_.emplace(env->executor.context()); | ||
| 127 | } | 127 | } | |||||
| HITCBC | 128 | 2 | catch(std::logic_error const&) | 128 | 2 | catch(std::logic_error const&) | ||
| 129 | { | 129 | { | |||||
| HITCBC | 130 | 2 | detail::throw_logic_error( | 130 | 2 | detail::throw_logic_error( | ||
| 131 | "delay requires an io_context-backed executor"); | 131 | "delay requires an io_context-backed executor"); | |||||
| HITCBC | 132 | 2 | } | 132 | 2 | } | ||
| MISUBC | 133 | ✗ | catch(std::exception const& e) | 133 | ✗ | catch(std::exception const& e) | ||
| 134 | { | 134 | { | |||||
| MISUBC | 135 | ✗ | detail::throw_logic_error(e.what()); | 135 | ✗ | detail::throw_logic_error(e.what()); | ||
| MISUBC | 136 | ✗ | } | 136 | ✗ | } | ||
| 137 | 137 | |||||||
| HITCBC | 138 | 7412 | if(has_deadline_) | 138 | 6417 | if(has_deadline_) | ||
| HITCBC | 139 | 2 | timer_->expires_at(deadline_); | 139 | 2 | timer_->expires_at(deadline_); | ||
| 140 | else | 140 | else | |||||
| HITCBC | 141 | 7410 | timer_->expires_after(dur_); | 141 | 6415 | timer_->expires_after(dur_); | ||
| 142 | 142 | |||||||
| HITCBC | 143 | 7412 | wait_.emplace(timer_->wait()); | 143 | 6417 | wait_.emplace(timer_->wait()); | ||
| HITCBC | 144 | 7412 | return wait_->await_suspend(h, env); | 144 | 6417 | return wait_->await_suspend(h, env); | ||
| 145 | } | 145 | } | |||||
| 146 | 146 | |||||||
| 147 | /// Return empty on expiry, `error::canceled` if stop won. | 147 | /// Return empty on expiry, `error::canceled` if stop won. | |||||
| HITCBC | 148 | 11364 | capy::io_result<> await_resume() noexcept | 148 | 10406 | capy::io_result<> await_resume() noexcept | ||
| 149 | { | 149 | { | |||||
| HITCBC | 150 | 11364 | if(canceled_) | 150 | 10406 | if(canceled_) | ||
| HITCBC | 151 | 3947 | return {capy::error::canceled}; | 151 | 4003 | return {capy::error::canceled}; | ||
| HITCBC | 152 | 7417 | if(wait_) | 152 | 6403 | if(wait_) | ||
| HITCBC | 153 | 7390 | return wait_->await_resume(); | 153 | 6395 | return wait_->await_resume(); | ||
| HITCBC | 154 | 27 | return {}; | 154 | 8 | return {}; | ||
| 155 | } | 155 | } | |||||
| 156 | }; | 156 | }; | |||||
| 157 | 157 | |||||||
| 158 | /** Suspend the current coroutine for a duration. | 158 | /** Suspend the current coroutine for a duration. | |||||
| 159 | 159 | |||||||
| 160 | Returns an IoAwaitable that completes at or after the | 160 | Returns an IoAwaitable that completes at or after the | |||||
| 161 | specified duration, or earlier if the environment's stop | 161 | specified duration, or earlier if the environment's stop | |||||
| 162 | token is activated. Zero or negative durations complete | 162 | token is activated. Zero or negative durations complete | |||||
| 163 | synchronously. | 163 | synchronously. | |||||
| 164 | 164 | |||||||
| 165 | @par Example | 165 | @par Example | |||||
| 166 | @code | 166 | @code | |||||
| 167 | auto [ec] = co_await delay(std::chrono::milliseconds(100)); | 167 | auto [ec] = co_await delay(std::chrono::milliseconds(100)); | |||||
| 168 | @endcode | 168 | @endcode | |||||
| 169 | 169 | |||||||
| 170 | @param dur The duration to wait. | 170 | @param dur The duration to wait. | |||||
| 171 | 171 | |||||||
| 172 | @return A @ref delay_awaitable yielding `io_result<>`. | 172 | @return A @ref delay_awaitable yielding `io_result<>`. | |||||
| 173 | */ | 173 | */ | |||||
| 174 | template<typename Rep, typename Period> | 174 | template<typename Rep, typename Period> | |||||
| 175 | [[nodiscard]] delay_awaitable | 175 | [[nodiscard]] delay_awaitable | |||||
| HITCBC | 176 | 11380 | delay(std::chrono::duration<Rep, Period> dur) noexcept | 176 | 10422 | delay(std::chrono::duration<Rep, Period> dur) noexcept | ||
| 177 | { | 177 | { | |||||
| 178 | using namespace std::chrono; | 178 | using namespace std::chrono; | |||||
| 179 | // Narrow reps wrap if nanoseconds::max() is converted into them; | 179 | // Narrow reps wrap if nanoseconds::max() is converted into them; | |||||
| 180 | // a double comparison clamps safely in both directions. | 180 | // a double comparison clamps safely in both directions. | |||||
| 181 | using dsec = duration<double>; | 181 | using dsec = duration<double>; | |||||
| HITCBC | 182 | 20708 | auto ns = dsec(dur) >= dsec((nanoseconds::max)()) | 182 | 18792 | auto ns = dsec(dur) >= dsec((nanoseconds::max)()) | ||
| HITCBC | 183 | 20708 | ? (nanoseconds::max)() | 183 | 18792 | ? (nanoseconds::max)() | ||
| HITCBC | 184 | 11378 | : dsec(dur) <= dsec((nanoseconds::min)()) | 184 | 10420 | : dsec(dur) <= dsec((nanoseconds::min)()) | ||
| HITCBC | 185 | 11378 | ? (nanoseconds::min)() | 185 | 10420 | ? (nanoseconds::min)() | ||
| HITCBC | 186 | 11376 | : duration_cast<nanoseconds>(dur); | 186 | 10418 | : duration_cast<nanoseconds>(dur); | ||
| HITCBC | 187 | 11380 | return delay_awaitable(ns); | 187 | 10422 | return delay_awaitable(ns); | ||
| 188 | } | 188 | } | |||||
| 189 | 189 | |||||||
| 190 | /** Suspend the current coroutine until a time point. | 190 | /** Suspend the current coroutine until a time point. | |||||
| 191 | 191 | |||||||
| 192 | Returns an IoAwaitable that completes at or after `tp`, or | 192 | Returns an IoAwaitable that completes at or after `tp`, or | |||||
| 193 | earlier if the environment's stop token is activated. Time | 193 | earlier if the environment's stop token is activated. Time | |||||
| 194 | points already reached complete synchronously. | 194 | points already reached complete synchronously. | |||||
| 195 | 195 | |||||||
| 196 | @param tp The steady-clock time point to wait until. | 196 | @param tp The steady-clock time point to wait until. | |||||
| 197 | 197 | |||||||
| 198 | @return A @ref delay_awaitable yielding `io_result<>`. | 198 | @return A @ref delay_awaitable yielding `io_result<>`. | |||||
| 199 | */ | 199 | */ | |||||
| 200 | [[nodiscard]] inline delay_awaitable | 200 | [[nodiscard]] inline delay_awaitable | |||||
| HITCBC | 201 | 6 | delay(std::chrono::steady_clock::time_point tp) noexcept | 201 | 6 | delay(std::chrono::steady_clock::time_point tp) noexcept | ||
| 202 | { | 202 | { | |||||
| HITCBC | 203 | 6 | return delay_awaitable(tp); | 203 | 6 | return delay_awaitable(tp); | ||
| 204 | } | 204 | } | |||||
| 205 | 205 | |||||||
| 206 | } // namespace boost::corosio | 206 | } // namespace boost::corosio | |||||
| 207 | 207 | |||||||
| 208 | #endif | 208 | #endif | |||||