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 1937 : void operator()() const noexcept
85 : {
86 1937 : src_->request_stop();
87 1937 : }
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 2052 : timeout_awaitable(A&& inner, std::chrono::nanoseconds dur)
113 2052 : : inner_(std::move(inner))
114 2052 : , dur_(dur)
115 2052 : , has_deadline_(false)
116 : {
117 2052 : }
118 :
119 4114 : ~timeout_awaitable()
120 : {
121 4114 : destroy_parent_cb();
122 4114 : }
123 :
124 : // Only moved before await_suspend, when cb_active_ is false
125 2058 : timeout_awaitable(timeout_awaitable&& o) noexcept(
126 : std::is_nothrow_move_constructible_v<A>)
127 2058 : : inner_(std::move(o.inner_))
128 2058 : , timer_(std::move(o.timer_))
129 2058 : , deadline_(o.deadline_)
130 2058 : , dur_(o.dur_)
131 2058 : , has_deadline_(o.has_deadline_)
132 2058 : , stop_src_(std::move(o.stop_src_))
133 : {
134 2058 : }
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 : // Forwarding here is load-bearing, not an optimization: awaitables
141 : // may perform setup in await_ready (type-erased stream wrappers
142 : // construct their cached inner op there), so the full awaiter
143 : // protocol must reach inner_ before await_suspend is driven. An
144 : // already-ready inner op also skips arming the timer entirely.
145 2054 : bool await_ready()
146 : {
147 2054 : return inner_.await_ready();
148 : }
149 :
150 2052 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
151 : {
152 2052 : parent_token_ = env->stop_token;
153 :
154 : // The deadline timer is built here from the awaiting
155 : // coroutine's executor context, the first point at which it
156 : // is known. await_suspend is driven through a noexcept
157 : // wrapper, so a failure cannot be surfaced as a catchable
158 : // exception. An executor whose context is not an io_context
159 : // cannot supply a timer service; silently running the
160 : // operation with no deadline would be a worse failure than
161 : // aborting, so translate the service-lookup error into a
162 : // clear precondition diagnostic. This terminates by design
163 : // (a usage error) rather than dropping the requested timeout.
164 : // The detached timeout coroutine must own its executor by
165 : // value (see timeout_coro::set_env_owned); io_env carries
166 : // only a non-owning executor_ref. Recover the concrete
167 : // executor from the context rather than the executor_ref:
168 : // wrapped executors (a strand over the io_context) satisfy
169 : // the documented precondition but do not expose the io
170 : // executor as their target. The timer construction below
171 : // validates the context is an io_context, and the detached
172 : // coroutine shares only the thread-safe stop_source with
173 : // the caller, so resuming it on the raw io executor instead
174 : // of the caller's wrapper is safe.
175 : try
176 : {
177 2052 : timer_.emplace(env->executor.context());
178 : }
179 4 : catch (std::logic_error const&)
180 : {
181 2 : throw_logic_error(
182 : "timeout requires an io_context-backed executor");
183 : }
184 : auto ex = static_cast<io_context&>(
185 2050 : env->executor.context()).get_executor();
186 :
187 2050 : if (has_deadline_)
188 4 : timer_->expires_at(deadline_);
189 : else
190 2046 : timer_->expires_after(dur_);
191 :
192 : // Launch fire-and-forget timeout (starts suspended)
193 2050 : auto timeout = make_timeout(*timer_, stop_src_);
194 4100 : timeout.h_.promise().set_env_owned(
195 2050 : ex, stop_src_.get_token(), env->frame_allocator);
196 : // Runs synchronously until timer.wait() suspends
197 2050 : timeout.h_.resume();
198 : // timeout goes out of scope; destructor is a no-op,
199 : // the coroutine self-destroys via suspend_never
200 :
201 : // Forward parent cancellation
202 2050 : new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_});
203 2050 : cb_active_ = true;
204 :
205 : // Start the inner op with our interposed stop_token
206 2050 : inner_env_ = {
207 2050 : env->executor, stop_src_.get_token(), env->frame_allocator};
208 4100 : return inner_.await_suspend(h, &inner_env_);
209 2050 : }
210 :
211 2050 : decltype(auto) await_resume()
212 : {
213 : // Read before request_stop: afterwards stop_requested()
214 : // can no longer distinguish who fired first. This must also
215 : // happen before inner_.await_resume() rather than after: when
216 : // the inner awaitable is itself a timeout_awaitable (nested
217 : // timeout()), our own request_stop() below is visible through
218 : // its parent_token_ (aliasing our stop_src_), and would
219 : // otherwise make its read of "parent" look like a
220 : // cancellation that never happened.
221 2050 : bool const parent = parent_token_.stop_requested();
222 2050 : bool const fired = stop_src_.stop_requested();
223 :
224 : // If inner_.await_resume() throws below, request_stop() is
225 : // skipped; the still-armed timeout coroutine is then drained
226 : // by timer_'s destructor rather than by us.
227 2050 : auto r = inner_.await_resume();
228 :
229 : // Cancel whichever is still pending (idempotent)
230 2048 : stop_src_.request_stop();
231 2048 : destroy_parent_cb();
232 :
233 : // Deadline won: stop_src_ is assumed to be the only
234 : // cancellation source, whose only writers are the timer
235 : // coroutine and the parent forwarder, so fired && !parent
236 : // identifies a timeout. A third-party cancellation of the
237 : // inner op (e.g. a socket cancel issued from elsewhere)
238 : // landing in the same window as the deadline firing is
239 : // reported as a timeout.
240 2070 : if (fired && !parent &&
241 2070 : r.ec == capy::cond::canceled)
242 : {
243 22 : std::remove_cvref_t<decltype(r)> t{};
244 22 : t.ec = make_error_code(capy::error::timeout);
245 22 : return t;
246 : }
247 2026 : return r;
248 : }
249 :
250 6162 : void destroy_parent_cb() noexcept
251 : {
252 6162 : if (cb_active_)
253 : {
254 2050 : std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
255 2050 : ->~stop_cb_type();
256 2050 : cb_active_ = false;
257 : }
258 6162 : }
259 : };
260 :
261 : } // namespace boost::corosio::detail
262 :
263 : #endif
|