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