1  
//
1  
//
2  
// Copyright (c) 2026 Michael Vandeberg
2  
// Copyright (c) 2026 Michael Vandeberg
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/capy
7  
// Official repository: https://github.com/cppalliance/capy
8  
//
8  
//
9  

9  

10  
#ifndef BOOST_CAPY_TIMEOUT_HPP
10  
#ifndef BOOST_CAPY_TIMEOUT_HPP
11  
#define BOOST_CAPY_TIMEOUT_HPP
11  
#define BOOST_CAPY_TIMEOUT_HPP
12  

12  

13  
#include <boost/capy/detail/config.hpp>
13  
#include <boost/capy/detail/config.hpp>
14  
#include <boost/capy/concept/io_awaitable.hpp>
14  
#include <boost/capy/concept/io_awaitable.hpp>
15  
#include <boost/capy/delay.hpp>
15  
#include <boost/capy/delay.hpp>
 
16 +
#include <boost/capy/detail/io_result_combinators.hpp>
16  
#include <boost/capy/error.hpp>
17  
#include <boost/capy/error.hpp>
17  
#include <boost/capy/io_result.hpp>
18  
#include <boost/capy/io_result.hpp>
18  
#include <boost/capy/task.hpp>
19  
#include <boost/capy/task.hpp>
19 -
#include <boost/capy/when_any.hpp>
20 +
#include <boost/capy/when_all.hpp>
20  

21  

 
22 +
#include <atomic>
21  
#include <chrono>
23  
#include <chrono>
22 -
#include <system_error>
24 +
#include <exception>
23 -
#include <type_traits>
25 +
#include <optional>
24  

26  

25  
namespace boost {
27  
namespace boost {
26 -

 
27  
namespace capy {
28  
namespace capy {
28  
namespace detail {
29  
namespace detail {
29  

30  

30  
template<typename T>
31  
template<typename T>
31 -
struct is_io_result : std::false_type {};
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<std::coroutine_handle<>, 2> runner_handles_{};
32  

39  

33 -
template<typename... Args>
40 +
    timeout_state()
34 -
struct is_io_result<io_result<Args...>> : std::true_type {};
41 +
        : core_(2)
 
42 +
    {
 
43 +
    }
 
44 +
};
35  

45  

36 -
template<typename T>
46 +
template<IoAwaitable Awaitable, typename T>
37 -
inline constexpr bool is_io_result_v = is_io_result<T>::value;
47 +
when_all_runner<timeout_state<T>>
 
48 +
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 +
    }
38  

60  

39 -
} // detail
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();
 
65 +
}
40  

66  

41 -
/** Race an awaitable against a deadline.
67 +
template<typename DelayAw, typename T>
 
68 +
when_all_runner<timeout_state<T>>
 
69 +
make_timeout_delay_runner(
 
70 +
    DelayAw d, timeout_state<T>* state)
 
71 +
{
 
72 +
    auto result = co_await std::move(d);
42  

73  

43 -
    Starts the awaitable and a timer concurrently. If the
74 +
    if(!result.ec)
44 -
    awaitable completes first, its result is returned. If the
75 +
    {
45 -
    timer fires first, stop is requested for the awaitable and
76 +
        int expected = -1;
46 -
    a timeout error is produced.
77 +
        if(state->winner_.compare_exchange_strong(
 
78 +
            expected, 1, std::memory_order_relaxed))
 
79 +
            state->core_.stop_source_.request_stop();
 
80 +
    }
 
81 +
}
47  

82  

48 -
    @par Return Type
83 +
template<IoAwaitable Inner, typename DelayAw, typename T>
 
84 +
class timeout_launcher
 
85 +
{
 
86 +
    Inner* inner_;
 
87 +
    DelayAw* delay_;
 
88 +
    timeout_state<T>* state_;
49  

89  

50 -
    The return type matches the inner awaitable's result type:
90 +
public:
 
91 +
    timeout_launcher(
 
92 +
        Inner* inner, DelayAw* delay,
 
93 +
        timeout_state<T>* state)
 
94 +
        : inner_(inner)
 
95 +
        , delay_(delay)
 
96 +
        , state_(state)
 
97 +
    {
 
98 +
    }
51  

99  

52 -
    @li For `io_result<...>` types: returns `io_result` with
100 +
    bool await_ready() const noexcept { return false; }
53 -
        `ec == error::timeout` and default-initialized values
101 +

54 -
    @li For non-void types: throws `std::system_error(error::timeout)`
102 +
    std::coroutine_handle<> await_suspend(
55 -
    @li For void: throws `std::system_error(error::timeout)`
103 +
        std::coroutine_handle<> continuation,
 
104 +
        io_env const* caller_env)
 
105 +
    {
 
106 +
        state_->core_.continuation_ = continuation;
 
107 +
        state_->core_.caller_env_ = caller_env;
 
108 +

 
109 +
        if(caller_env->stop_token.stop_possible())
 
110 +
        {
 
111 +
            state_->core_.parent_stop_callback_.emplace(
 
112 +
                caller_env->stop_token,
 
113 +
                when_all_core::stop_callback_fn{
 
114 +
                    &state_->core_.stop_source_});
 
115 +

 
116 +
            if(caller_env->stop_token.stop_requested())
 
117 +
                state_->core_.stop_source_.request_stop();
 
118 +
        }
 
119 +

 
120 +
        auto token = state_->core_.stop_source_.get_token();
 
121 +

 
122 +
        auto r0 = make_timeout_inner_runner(
 
123 +
            std::move(*inner_), state_);
 
124 +
        auto h0 = r0.release();
 
125 +
        h0.promise().state_ = state_;
 
126 +
        h0.promise().env_ = io_env{
 
127 +
            caller_env->executor, token,
 
128 +
            caller_env->frame_allocator};
 
129 +
        state_->runner_handles_[0] =
 
130 +
            std::coroutine_handle<>{h0};
 
131 +

 
132 +
        auto r1 = make_timeout_delay_runner(
 
133 +
            std::move(*delay_), state_);
 
134 +
        auto h1 = r1.release();
 
135 +
        h1.promise().state_ = state_;
 
136 +
        h1.promise().env_ = io_env{
 
137 +
            caller_env->executor, token,
 
138 +
            caller_env->frame_allocator};
 
139 +
        state_->runner_handles_[1] =
 
140 +
            std::coroutine_handle<>{h1};
 
141 +

 
142 +
        caller_env->executor.post(
 
143 +
            state_->runner_handles_[0]);
 
144 +
        caller_env->executor.post(
 
145 +
            state_->runner_handles_[1]);
 
146 +

 
147 +
        return std::noop_coroutine();
 
148 +
    }
 
149 +

 
150 +
    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.
56  

171  

57  
    @par Precision
172  
    @par Precision
58  

173  

59  
    The timeout fires at or after the specified duration.
174  
    The timeout fires at or after the specified duration.
60  

175  

61  
    @par Cancellation
176  
    @par Cancellation
62  

177  

63 -
    If the parent's stop token is activated, the inner awaitable
178 +
    If the parent's stop token is activated, both children are
64 -
    is cancelled normally (not a timeout). The result reflects
179 +
    cancelled. The inner awaitable's cancellation result is
65 -
    the inner awaitable's cancellation behavior.
180 +
    returned.
66  

181  

67  
    @par Example
182  
    @par Example
68  
    @code
183  
    @code
69  
    auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms);
184  
    auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms);
70  
    if (ec == cond::timeout) {
185  
    if (ec == cond::timeout) {
71  
        // handle timeout
186  
        // handle timeout
72  
    }
187  
    }
73  
    @endcode
188  
    @endcode
74  

189  

75 -
    @tparam A An IoAwaitable whose result type determines
190 +
    @tparam A An IoAwaitable returning `io_result<Ts...>`.
76 -
        how timeouts are reported.
 
77  

191  

78  
    @param a The awaitable to race against the deadline.
192  
    @param a The awaitable to race against the deadline.
79  
    @param dur The maximum duration to wait.
193  
    @param dur The maximum duration to wait.
80  

194  

81  
    @return `task<awaitable_result_t<A>>`.
195  
    @return `task<awaitable_result_t<A>>`.
82  

196  

83 -
    @throws std::system_error with `error::timeout` if the timer
197 +
    @throws Rethrows any exception from the inner awaitable,
84 -
        fires first and the result type is not `io_result`.
198 +
        regardless of whether the timer has fired.
85 -
        Exceptions thrown by the inner awaitable propagate
 
86 -
        unchanged.
 
87  

199  

88 -
    @see delay, when_any, cond::timeout
200 +
    @see delay, cond::timeout
89  
*/
201  
*/
90  
template<IoAwaitable A, typename Rep, typename Period>
202  
template<IoAwaitable A, typename Rep, typename Period>
 
203 +
    requires detail::is_io_result_v<awaitable_result_t<A>>
91  
auto timeout(A a, std::chrono::duration<Rep, Period> dur)
204  
auto timeout(A a, std::chrono::duration<Rep, Period> dur)
92  
    -> task<awaitable_result_t<A>>
205  
    -> task<awaitable_result_t<A>>
93  
{
206  
{
94  
    using T = awaitable_result_t<A>;
207  
    using T = awaitable_result_t<A>;
95  

208  

96 -
    auto result = co_await when_any(
209 +
    auto d = delay(dur);
97 -
        std::move(a), delay(dur));
210 +
    detail::timeout_state<T> state;
98  

211  

99 -
    if(result.index() == 0)
212 +
    co_await detail::timeout_launcher<
100 -
    {
213 +
        A, decltype(d), T>(&a, &d, &state);
101 -
        // Task completed first
214 +

102 -
        if constexpr (std::is_void_v<T>)
215 +
    if(state.core_.first_exception_)
103 -
            co_return;
216 +
        std::rethrow_exception(state.core_.first_exception_);
104 -
        else
217 +
    if(state.inner_exception_)
105 -
            co_return std::get<0>(std::move(result));
218 +
        std::rethrow_exception(state.inner_exception_);
106 -
    }
219 +

107 -
    else
220 +
    if(state.winner_.load(std::memory_order_relaxed) == 0)
108 -
    {
221 +
        co_return std::move(*state.inner_result_);
109 -
        // Timer won
222 +

110 -
        if constexpr (detail::is_io_result_v<T>)
223 +
    // Delay fired first: timeout
111 -
        {
224 +
    T r{};
112 -
            T timeout_result{};
225 +
    r.ec = make_error_code(error::timeout);
113 -
            timeout_result.ec = make_error_code(error::timeout);
226 +
    co_return r;
114 -
            co_return timeout_result;
 
115 -
        }
 
116 -
        else if constexpr (std::is_void_v<T>)
 
117 -
        {
 
118 -
            throw std::system_error(
 
119 -
                make_error_code(error::timeout));
 
120 -
        }
 
121 -
        else
 
122 -
        {
 
123 -
            throw std::system_error(
 
124 -
                make_error_code(error::timeout));
 
125 -
        }
 
126 -
    }
 
127  
}
227  
}
128  

228  

129  
} // capy
229  
} // capy
130  
} // boost
230  
} // boost
131  

231  

132  
#endif
232  
#endif