TLA Line data Source code
1 : //
2 : // Copyright (c) 2026 Steve Gerbino
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/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_DETAIL_TIMEOUT_AWAITABLE_HPP
11 : #define BOOST_COROSIO_DETAIL_TIMEOUT_AWAITABLE_HPP
12 :
13 : #include <boost/corosio/io_context.hpp>
14 : #include <boost/corosio/detail/timeout_coro.hpp>
15 : #include <boost/corosio/detail/timer.hpp>
16 : #include <boost/corosio/detail/except.hpp>
17 : #include <boost/capy/cond.hpp>
18 : #include <boost/capy/error.hpp>
19 : #include <boost/capy/ex/io_env.hpp>
20 : #include <boost/capy/io_result.hpp>
21 :
22 : #include <chrono>
23 : #include <coroutine>
24 : #include <new>
25 : #include <optional>
26 : #include <stdexcept>
27 : #include <stop_token>
28 : #include <type_traits>
29 : #include <utility>
30 :
31 : /* Races an inner IoAwaitable against a timer via a shared
32 : stop_source. await_suspend arms the timer by launching a
33 : fire-and-forget timeout_coro, then starts the inner op with
34 : an interposed stop_token. Whichever completes first signals
35 : the stop_source, cancelling the other.
36 :
37 : Parent cancellation is forwarded through a stop_callback
38 : stored in a placement-new buffer (stop_callback is not
39 : movable, but the awaitable must be movable for
40 : transform_awaiter). The buffer is inert during moves
41 : (before await_suspend) and constructed in-place once the
42 : awaitable is pinned on the coroutine frame.
43 :
44 : The timeout_coro can outlive this awaitable — it owns its
45 : env and self-destroys via suspend_never. The timer lives in
46 : std::optional and is constructed lazily in await_suspend,
47 : once the awaiting coroutine's executor context is known. */
48 :
49 : namespace boost::corosio::detail {
50 :
51 : // Local stand-in for capy::detail's io_result trait: corosio must not
52 : // reach into capy::detail, but the result-mapping switch in
53 : // await_resume needs to distinguish io_result from other return types.
54 : template<typename T>
55 : struct is_io_result : std::false_type
56 : {
57 : };
58 :
59 : template<typename... Ts>
60 : struct is_io_result<capy::io_result<Ts...>> : std::true_type
61 : {
62 : };
63 :
64 : template<typename T>
65 : inline constexpr bool is_io_result_v = is_io_result<T>::value;
66 :
67 : /** Awaitable adapter that cancels an inner operation after a deadline.
68 :
69 : Races the inner awaitable against a timer. A shared stop_source
70 : ties them together: whichever completes first cancels the other.
71 : Parent cancellation is forwarded via stop_callback.
72 :
73 : The timer is constructed internally in `await_suspend` from the
74 : execution context in `io_env`.
75 :
76 : @tparam A The inner IoAwaitable type (decayed).
77 : */
78 : template<typename A>
79 : struct timeout_awaitable
80 : {
81 : struct stop_forwarder
82 : {
83 : std::stop_source* src_;
84 HIT 2004 : void operator()() const noexcept
85 : {
86 2004 : src_->request_stop();
87 2004 : }
88 : };
89 :
90 : using time_point = std::chrono::steady_clock::time_point;
91 : using stop_cb_type = std::stop_callback<stop_forwarder>;
92 :
93 : A inner_;
94 : std::optional<timer> timer_;
95 : time_point deadline_;
96 : std::chrono::nanoseconds dur_{};
97 : bool has_deadline_ = true;
98 : std::stop_source stop_src_;
99 : std::stop_token parent_token_;
100 : capy::io_env inner_env_;
101 : alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)];
102 : bool cb_active_ = false;
103 :
104 : /// Construct without a timer, deadline given as an absolute time.
105 4 : timeout_awaitable(A&& inner, time_point deadline)
106 4 : : inner_(std::move(inner))
107 4 : , deadline_(deadline)
108 : {
109 4 : }
110 :
111 : /// Construct without a timer, deadline measured from suspension.
112 2038 : timeout_awaitable(A&& inner, std::chrono::nanoseconds dur)
113 2038 : : inner_(std::move(inner))
114 2038 : , dur_(dur)
115 2038 : , has_deadline_(false)
116 : {
117 2038 : }
118 :
119 4084 : ~timeout_awaitable()
120 : {
121 4084 : destroy_parent_cb();
122 4084 : }
123 :
124 : // Only moved before await_suspend, when cb_active_ is false
125 2042 : timeout_awaitable(timeout_awaitable&& o) noexcept(
126 : std::is_nothrow_move_constructible_v<A>)
127 2042 : : inner_(std::move(o.inner_))
128 2042 : , timer_(std::move(o.timer_))
129 2042 : , deadline_(o.deadline_)
130 2042 : , dur_(o.dur_)
131 2042 : , has_deadline_(o.has_deadline_)
132 2042 : , stop_src_(std::move(o.stop_src_))
133 : {
134 2042 : }
135 :
136 : timeout_awaitable(timeout_awaitable const&) = delete;
137 : timeout_awaitable& operator=(timeout_awaitable const&) = delete;
138 : timeout_awaitable& operator=(timeout_awaitable&&) = delete;
139 :
140 2038 : bool await_ready() const noexcept
141 : {
142 2038 : return false;
143 : }
144 :
145 2042 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
146 : {
147 2042 : parent_token_ = env->stop_token;
148 :
149 : // The deadline timer is built here from the awaiting
150 : // coroutine's executor context, the first point at which it
151 : // is known. await_suspend is driven through a noexcept
152 : // wrapper, so a failure cannot be surfaced as a catchable
153 : // exception. An executor whose context is not an io_context
154 : // cannot supply a timer service; silently running the
155 : // operation with no deadline would be a worse failure than
156 : // aborting, so translate the service-lookup error into a
157 : // clear precondition diagnostic. This terminates by design
158 : // (a usage error) rather than dropping the requested timeout.
159 : // The detached timeout coroutine must own its executor by
160 : // value (see timeout_coro::set_env_owned); io_env carries
161 : // only a non-owning executor_ref. Recover the concrete
162 : // executor from the context rather than the executor_ref:
163 : // wrapped executors (a strand over the io_context) satisfy
164 : // the documented precondition but do not expose the io
165 : // executor as their target. The timer construction below
166 : // validates the context is an io_context, and the detached
167 : // coroutine shares only the thread-safe stop_source with
168 : // the caller, so resuming it on the raw io executor instead
169 : // of the caller's wrapper is safe.
170 : try
171 : {
172 2042 : timer_.emplace(env->executor.context());
173 : }
174 4 : catch (std::logic_error const&)
175 : {
176 2 : throw_logic_error(
177 : "timeout requires an io_context-backed executor");
178 : }
179 : auto ex = static_cast<io_context&>(
180 2040 : env->executor.context()).get_executor();
181 :
182 2040 : if (has_deadline_)
183 4 : timer_->expires_at(deadline_);
184 : else
185 2036 : timer_->expires_after(dur_);
186 :
187 : // Launch fire-and-forget timeout (starts suspended)
188 2040 : auto timeout = make_timeout(*timer_, stop_src_);
189 4080 : timeout.h_.promise().set_env_owned(
190 2040 : ex, stop_src_.get_token(), env->frame_allocator);
191 : // Runs synchronously until timer.wait() suspends
192 2040 : timeout.h_.resume();
193 : // timeout goes out of scope; destructor is a no-op,
194 : // the coroutine self-destroys via suspend_never
195 :
196 : // Forward parent cancellation
197 2040 : new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_});
198 2040 : cb_active_ = true;
199 :
200 : // Start the inner op with our interposed stop_token
201 2040 : inner_env_ = {
202 2040 : env->executor, stop_src_.get_token(), env->frame_allocator};
203 4080 : return inner_.await_suspend(h, &inner_env_);
204 2040 : }
205 :
206 2036 : decltype(auto) await_resume()
207 : {
208 : // Read before request_stop: afterwards stop_requested()
209 : // can no longer distinguish who fired first. This must also
210 : // happen before inner_.await_resume() rather than after: when
211 : // the inner awaitable is itself a timeout_awaitable (nested
212 : // timeout()), our own request_stop() below is visible through
213 : // its parent_token_ (aliasing our stop_src_), and would
214 : // otherwise make its read of "parent" look like a
215 : // cancellation that never happened.
216 2036 : bool const parent = parent_token_.stop_requested();
217 2036 : bool const fired = stop_src_.stop_requested();
218 :
219 : // If inner_.await_resume() throws below, request_stop() is
220 : // skipped; the still-armed timeout coroutine is then drained
221 : // by timer_'s destructor rather than by us.
222 2036 : auto r = inner_.await_resume();
223 :
224 : // Cancel whichever is still pending (idempotent)
225 2034 : stop_src_.request_stop();
226 2034 : destroy_parent_cb();
227 :
228 : // Deadline won: stop_src_ is assumed to be the only
229 : // cancellation source, whose only writers are the timer
230 : // coroutine and the parent forwarder, so fired && !parent
231 : // identifies a timeout. A third-party cancellation of the
232 : // inner op (e.g. a socket cancel issued from elsewhere)
233 : // landing in the same window as the deadline firing is
234 : // reported as a timeout.
235 2054 : if (fired && !parent &&
236 2054 : r.ec == capy::cond::canceled)
237 : {
238 20 : std::remove_cvref_t<decltype(r)> t{};
239 20 : t.ec = make_error_code(capy::error::timeout);
240 20 : return t;
241 : }
242 2014 : return r;
243 : }
244 :
245 6118 : void destroy_parent_cb() noexcept
246 : {
247 6118 : if (cb_active_)
248 : {
249 2040 : std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
250 2040 : ->~stop_cb_type();
251 2040 : cb_active_ = false;
252 : }
253 6118 : }
254 : };
255 :
256 : } // namespace boost::corosio::detail
257 :
258 : #endif
|