89.13% Lines (41/46)
100.00% Functions (8/8)
| 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_TIMEOUT_HPP | ||||||
| 11 | + | #define BOOST_CAPY_TIMEOUT_HPP | ||||||
| 12 | + | |||||||
| 13 | + | #include <boost/capy/detail/config.hpp> | ||||||
| 14 | + | #include <boost/capy/concept/io_awaitable.hpp> | ||||||
| 15 | + | #include <boost/capy/delay.hpp> | ||||||
| 16 | + | #include <boost/capy/detail/io_result_combinators.hpp> | ||||||
| 17 | + | #include <boost/capy/error.hpp> | ||||||
| 18 | + | #include <boost/capy/io_result.hpp> | ||||||
| 19 | + | #include <boost/capy/task.hpp> | ||||||
| 20 | + | #include <boost/capy/when_all.hpp> | ||||||
| 21 | + | |||||||
| 22 | + | #include <atomic> | ||||||
| 23 | + | #include <chrono> | ||||||
| 24 | + | #include <exception> | ||||||
| 25 | + | #include <optional> | ||||||
| 26 | + | |||||||
| 27 | + | namespace boost { | ||||||
| 28 | + | namespace capy { | ||||||
| 29 | + | namespace detail { | ||||||
| 30 | + | |||||||
| 31 | + | template<typename T> | ||||||
| 32 | + | struct timeout_state | ||||||
| 33 | + | { | ||||||
| 34 | + | when_all_core core_; | ||||||
| 35 | + | std::atomic<int> winner_{-1}; // -1=none, 0=inner, 1=delay | ||||||
| 36 | + | std::optional<T> inner_result_; | ||||||
| 37 | + | std::exception_ptr inner_exception_; | ||||||
| 38 | + | std::array<continuation, 2> runner_handles_{}; | ||||||
| 39 | + | |||||||
| HITGNC | 40 | + | 8 | timeout_state() | ||||
| HITGNC | 41 | + | 8 | : core_(2) | ||||
| 42 | + | { | ||||||
| HITGNC | 43 | + | 8 | } | ||||
| 44 | + | }; | ||||||
| 45 | + | |||||||
| 46 | + | template<IoAwaitable Awaitable, typename T> | ||||||
| 47 | + | when_all_runner<timeout_state<T>> | ||||||
| HITGNC | 48 | + | 8 | make_timeout_inner_runner( | ||||
| 49 | + | Awaitable inner, timeout_state<T>* state) | ||||||
| 50 | + | { | ||||||
| 51 | + | try | ||||||
| 52 | + | { | ||||||
| 53 | + | auto result = co_await std::move(inner); | ||||||
| 54 | + | state->inner_result_.emplace(std::move(result)); | ||||||
| 55 | + | } | ||||||
| 56 | + | catch(...) | ||||||
| 57 | + | { | ||||||
| 58 | + | state->inner_exception_ = std::current_exception(); | ||||||
| 59 | + | } | ||||||
| 60 | + | |||||||
| 61 | + | int expected = -1; | ||||||
| 62 | + | if(state->winner_.compare_exchange_strong( | ||||||
| 63 | + | expected, 0, std::memory_order_relaxed)) | ||||||
| 64 | + | state->core_.stop_source_.request_stop(); | ||||||
| HITGNC | 65 | + | 16 | } | ||||
| 66 | + | |||||||
| 67 | + | template<typename DelayAw, typename T> | ||||||
| 68 | + | when_all_runner<timeout_state<T>> | ||||||
| HITGNC | 69 | + | 8 | make_timeout_delay_runner( | ||||
| 70 | + | DelayAw d, timeout_state<T>* state) | ||||||
| 71 | + | { | ||||||
| 72 | + | auto result = co_await std::move(d); | ||||||
| 73 | + | |||||||
| 74 | + | if(!result.ec) | ||||||
| 75 | + | { | ||||||
| 76 | + | int expected = -1; | ||||||
| 77 | + | if(state->winner_.compare_exchange_strong( | ||||||
| 78 | + | expected, 1, std::memory_order_relaxed)) | ||||||
| 79 | + | state->core_.stop_source_.request_stop(); | ||||||
| 80 | + | } | ||||||
| HITGNC | 81 | + | 16 | } | ||||
| 82 | + | |||||||
| 83 | + | template<IoAwaitable Inner, typename DelayAw, typename T> | ||||||
| 84 | + | class timeout_launcher | ||||||
| 85 | + | { | ||||||
| 86 | + | Inner* inner_; | ||||||
| 87 | + | DelayAw* delay_; | ||||||
| 88 | + | timeout_state<T>* state_; | ||||||
| 89 | + | |||||||
| 90 | + | public: | ||||||
| HITGNC | 91 | + | 8 | timeout_launcher( | ||||
| 92 | + | Inner* inner, DelayAw* delay, | ||||||
| 93 | + | timeout_state<T>* state) | ||||||
| HITGNC | 94 | + | 8 | : inner_(inner) | ||||
| HITGNC | 95 | + | 8 | , delay_(delay) | ||||
| HITGNC | 96 | + | 8 | , state_(state) | ||||
| 97 | + | { | ||||||
| HITGNC | 98 | + | 8 | } | ||||
| 99 | + | |||||||
| HITGNC | 100 | + | 8 | bool await_ready() const noexcept { return false; } | ||||
| 101 | + | |||||||
| HITGNC | 102 | + | 8 | std::coroutine_handle<> await_suspend( | ||||
| 103 | + | std::coroutine_handle<> continuation, | ||||||
| 104 | + | io_env const* caller_env) | ||||||
| 105 | + | { | ||||||
| HITGNC | 106 | + | 8 | state_->core_.continuation_.h = continuation; | ||||
| HITGNC | 107 | + | 8 | state_->core_.caller_env_ = caller_env; | ||||
| 108 | + | |||||||
| HITGNC | 109 | + | 8 | if(caller_env->stop_token.stop_possible()) | ||||
| 110 | + | { | ||||||
| MISUNC | 111 | + | ✗ | state_->core_.parent_stop_callback_.emplace( | ||||
| MISUNC | 112 | + | ✗ | caller_env->stop_token, | ||||
| 113 | + | when_all_core::stop_callback_fn{ | ||||||
| MISUNC | 114 | + | ✗ | &state_->core_.stop_source_}); | ||||
| 115 | + | |||||||
| MISUNC | 116 | + | ✗ | if(caller_env->stop_token.stop_requested()) | ||||
| MISUNC | 117 | + | ✗ | state_->core_.stop_source_.request_stop(); | ||||
| 118 | + | } | ||||||
| 119 | + | |||||||
| HITGNC | 120 | + | 8 | auto token = state_->core_.stop_source_.get_token(); | ||||
| 121 | + | |||||||
| HITGNC | 122 | + | 8 | auto r0 = make_timeout_inner_runner( | ||||
| HITGNC | 123 | + | 8 | std::move(*inner_), state_); | ||||
| HITGNC | 124 | + | 8 | auto h0 = r0.release(); | ||||
| HITGNC | 125 | + | 8 | h0.promise().state_ = state_; | ||||
| HITGNC | 126 | + | 8 | h0.promise().env_ = io_env{ | ||||
| 127 | + | caller_env->executor, token, | ||||||
| HITGNC | 128 | + | 8 | caller_env->frame_allocator}; | ||||
| HITGNC | 129 | + | 8 | state_->runner_handles_[0].h = | ||||
| 130 | + | std::coroutine_handle<>{h0}; | ||||||
| 131 | + | |||||||
| HITGNC | 132 | + | 8 | auto r1 = make_timeout_delay_runner( | ||||
| HITGNC | 133 | + | 8 | std::move(*delay_), state_); | ||||
| HITGNC | 134 | + | 8 | auto h1 = r1.release(); | ||||
| HITGNC | 135 | + | 8 | h1.promise().state_ = state_; | ||||
| HITGNC | 136 | + | 8 | h1.promise().env_ = io_env{ | ||||
| 137 | + | caller_env->executor, token, | ||||||
| HITGNC | 138 | + | 8 | caller_env->frame_allocator}; | ||||
| HITGNC | 139 | + | 8 | state_->runner_handles_[1].h = | ||||
| 140 | + | std::coroutine_handle<>{h1}; | ||||||
| 141 | + | |||||||
| HITGNC | 142 | + | 8 | caller_env->executor.post( | ||||
| HITGNC | 143 | + | 8 | state_->runner_handles_[0]); | ||||
| HITGNC | 144 | + | 8 | caller_env->executor.post( | ||||
| HITGNC | 145 | + | 8 | state_->runner_handles_[1]); | ||||
| 146 | + | |||||||
| HITGNC | 147 | + | 16 | return std::noop_coroutine(); | ||||
| HITGNC | 148 | + | 24 | } | ||||
| 149 | + | |||||||
| HITGNC | 150 | + | 8 | void await_resume() const noexcept {} | ||||
| 151 | + | }; | ||||||
| 152 | + | |||||||
| 153 | + | } // namespace detail | ||||||
| 154 | + | |||||||
| 155 | + | /** Race an io_result-returning awaitable against a deadline. | ||||||
| 156 | + | |||||||
| 157 | + | Starts the awaitable and a timer concurrently. The first to | ||||||
| 158 | + | complete wins and cancels the other. If the awaitable finishes | ||||||
| 159 | + | first, its result is returned as-is (success, error, or | ||||||
| 160 | + | exception). If the timer fires first, an `io_result` with | ||||||
| 161 | + | `ec == error::timeout` is produced. | ||||||
| 162 | + | |||||||
| 163 | + | Unlike @ref when_any, exceptions from the inner awaitable | ||||||
| 164 | + | are always propagated — they are never swallowed by the timer. | ||||||
| 165 | + | |||||||
| 166 | + | @par Return Type | ||||||
| 167 | + | |||||||
| 168 | + | Always returns `io_result<Ts...>` matching the inner | ||||||
| 169 | + | awaitable's result type. On timeout, `ec` is set to | ||||||
| 170 | + | `error::timeout` and payload values are default-initialized. | ||||||
| 171 | + | |||||||
| 172 | + | @par Precision | ||||||
| 173 | + | |||||||
| 174 | + | The timeout fires at or after the specified duration. | ||||||
| 175 | + | |||||||
| 176 | + | @par Cancellation | ||||||
| 177 | + | |||||||
| 178 | + | If the parent's stop token is activated, both children are | ||||||
| 179 | + | cancelled. The inner awaitable's cancellation result is | ||||||
| 180 | + | returned. | ||||||
| 181 | + | |||||||
| 182 | + | @par Example | ||||||
| 183 | + | @code | ||||||
| 184 | + | auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms); | ||||||
| 185 | + | if (ec == cond::timeout) { | ||||||
| 186 | + | // handle timeout | ||||||
| 187 | + | } | ||||||
| 188 | + | @endcode | ||||||
| 189 | + | |||||||
| 190 | + | @tparam A An IoAwaitable returning `io_result<Ts...>`. | ||||||
| 191 | + | |||||||
| 192 | + | @param a The awaitable to race against the deadline. | ||||||
| 193 | + | @param dur The maximum duration to wait. | ||||||
| 194 | + | |||||||
| 195 | + | @return `task<awaitable_result_t<A>>`. | ||||||
| 196 | + | |||||||
| 197 | + | @throws Rethrows any exception from the inner awaitable, | ||||||
| 198 | + | regardless of whether the timer has fired. | ||||||
| 199 | + | |||||||
| 200 | + | @see delay, cond::timeout | ||||||
| 201 | + | */ | ||||||
| 202 | + | template<IoAwaitable A, typename Rep, typename Period> | ||||||
| 203 | + | requires detail::is_io_result_v<awaitable_result_t<A>> | ||||||
| HITGNC | 204 | + | 8 | auto timeout(A a, std::chrono::duration<Rep, Period> dur) | ||||
| 205 | + | -> task<awaitable_result_t<A>> | ||||||
| 206 | + | { | ||||||
| 207 | + | using T = awaitable_result_t<A>; | ||||||
| 208 | + | |||||||
| 209 | + | auto d = delay(dur); | ||||||
| 210 | + | detail::timeout_state<T> state; | ||||||
| 211 | + | |||||||
| 212 | + | co_await detail::timeout_launcher< | ||||||
| 213 | + | A, decltype(d), T>(&a, &d, &state); | ||||||
| 214 | + | |||||||
| 215 | + | if(state.core_.first_exception_) | ||||||
| 216 | + | std::rethrow_exception(state.core_.first_exception_); | ||||||
| 217 | + | if(state.inner_exception_) | ||||||
| 218 | + | std::rethrow_exception(state.inner_exception_); | ||||||
| 219 | + | |||||||
| 220 | + | if(state.winner_.load(std::memory_order_relaxed) == 0) | ||||||
| 221 | + | co_return std::move(*state.inner_result_); | ||||||
| 222 | + | |||||||
| 223 | + | // Delay fired first: timeout | ||||||
| 224 | + | T r{}; | ||||||
| 225 | + | r.ec = make_error_code(error::timeout); | ||||||
| 226 | + | co_return r; | ||||||
| HITGNC | 227 | + | 16 | } | ||||
| 228 | + | |||||||
| 229 | + | } // capy | ||||||
| 230 | + | } // boost | ||||||
| 231 | + | |||||||
| 232 | + | #endif | ||||||