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_CANCEL_AT_AWAITABLE_HPP
11 : #define BOOST_COROSIO_DETAIL_CANCEL_AT_AWAITABLE_HPP
12 :
13 : #include <boost/corosio/detail/timeout_coro.hpp>
14 : #include <boost/corosio/detail/except.hpp>
15 : #include <boost/capy/ex/io_env.hpp>
16 :
17 : #include <chrono>
18 : #include <coroutine>
19 : #include <new>
20 : #include <optional>
21 : #include <stdexcept>
22 : #include <stop_token>
23 : #include <type_traits>
24 : #include <utility>
25 :
26 : /* Races an inner IoAwaitable against a timer via a shared
27 : stop_source. await_suspend arms the timer by launching a
28 : fire-and-forget timeout_coro, then starts the inner op with
29 : an interposed stop_token. Whichever completes first signals
30 : the stop_source, cancelling the other.
31 :
32 : Parent cancellation is forwarded through a stop_callback
33 : stored in a placement-new buffer (stop_callback is not
34 : movable, but the awaitable must be movable for
35 : transform_awaiter). The buffer is inert during moves
36 : (before await_suspend) and constructed in-place once the
37 : awaitable is pinned on the coroutine frame.
38 :
39 : The timeout_coro can outlive this awaitable — it owns its
40 : env and self-destroys via suspend_never. When Owning is
41 : false the caller-supplied timer must outlive both; when
42 : Owning is true the timer lives in std::optional and is
43 : constructed lazily in await_suspend. */
44 :
45 : namespace boost::corosio::detail {
46 :
47 : /** Awaitable adapter that cancels an inner operation after a deadline.
48 :
49 : Races the inner awaitable against a timer. A shared stop_source
50 : ties them together: whichever completes first cancels the other.
51 : Parent cancellation is forwarded via stop_callback.
52 :
53 : When @p Owning is `false` (default), the caller supplies a timer
54 : reference that must outlive the awaitable. When @p Owning is
55 : `true`, the timer is constructed internally in `await_suspend`
56 : from the execution context in `io_env`.
57 :
58 : @tparam A The inner IoAwaitable type (decayed).
59 : @tparam Timer The timer type (`timer` or `native_timer<B>`).
60 : @tparam Owning When `true`, the awaitable owns its timer.
61 : */
62 : template<typename A, typename Timer, bool Owning = false>
63 : struct cancel_at_awaitable
64 : {
65 : struct stop_forwarder
66 : {
67 : std::stop_source* src_;
68 HIT 2 : void operator()() const noexcept
69 : {
70 2 : src_->request_stop();
71 2 : }
72 : };
73 :
74 : using time_point = std::chrono::steady_clock::time_point;
75 : using stop_cb_type = std::stop_callback<stop_forwarder>;
76 : using timer_storage =
77 : std::conditional_t<Owning, std::optional<Timer>, Timer*>;
78 :
79 : A inner_;
80 : timer_storage timer_;
81 : time_point deadline_;
82 : std::stop_source stop_src_;
83 : capy::io_env inner_env_;
84 : alignas(stop_cb_type) unsigned char cb_buf_[sizeof(stop_cb_type)];
85 : bool cb_active_ = false;
86 :
87 : /// Construct with a caller-supplied timer reference.
88 18 : cancel_at_awaitable(A&& inner, Timer& timer, time_point deadline)
89 : requires(!Owning)
90 18 : : inner_(std::move(inner))
91 18 : , timer_(&timer)
92 18 : , deadline_(deadline)
93 : {
94 18 : }
95 :
96 : /// Construct without a timer (created in `await_suspend`).
97 12 : cancel_at_awaitable(A&& inner, time_point deadline)
98 : requires Owning
99 12 : : inner_(std::move(inner))
100 12 : , deadline_(deadline)
101 : {
102 12 : }
103 :
104 60 : ~cancel_at_awaitable()
105 : {
106 60 : destroy_parent_cb();
107 60 : }
108 :
109 : // Only moved before await_suspend, when cb_active_ is false
110 30 : cancel_at_awaitable(cancel_at_awaitable&& o) noexcept(
111 : std::is_nothrow_move_constructible_v<A>)
112 30 : : inner_(std::move(o.inner_))
113 30 : , timer_(std::move(o.timer_))
114 30 : , deadline_(o.deadline_)
115 30 : , stop_src_(std::move(o.stop_src_))
116 : {
117 30 : }
118 :
119 : cancel_at_awaitable(cancel_at_awaitable const&) = delete;
120 : cancel_at_awaitable& operator=(cancel_at_awaitable const&) = delete;
121 : cancel_at_awaitable& operator=(cancel_at_awaitable&&) = delete;
122 :
123 30 : bool await_ready() const noexcept
124 : {
125 30 : return false;
126 : }
127 :
128 30 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
129 : {
130 : if constexpr (Owning)
131 : {
132 : // The deadline timer is built here from the awaiting
133 : // coroutine's executor context, the first point at which it
134 : // is known. await_suspend is driven through a noexcept
135 : // wrapper, so a failure cannot be surfaced as a catchable
136 : // exception. An executor whose context is not an io_context
137 : // cannot supply a timer service; silently running the
138 : // operation with no deadline would be a worse failure than
139 : // aborting, so translate the service-lookup error into a
140 : // clear precondition diagnostic. This terminates by design
141 : // (a usage error) rather than dropping the requested timeout.
142 : try
143 : {
144 12 : timer_.emplace(env->executor.context());
145 : }
146 MIS 0 : catch (std::logic_error const&)
147 : {
148 0 : throw_logic_error(
149 : "cancel_after/cancel_at requires an "
150 : "io_context-backed executor");
151 : }
152 : }
153 :
154 HIT 30 : timer_->expires_at(deadline_);
155 :
156 : // Launch fire-and-forget timeout (starts suspended)
157 30 : auto timeout = make_timeout(*timer_, stop_src_);
158 60 : timeout.h_.promise().set_env_owned(
159 30 : {env->executor, stop_src_.get_token(), env->frame_allocator});
160 : // Runs synchronously until timer.wait() suspends
161 30 : timeout.h_.resume();
162 : // timeout goes out of scope; destructor is a no-op,
163 : // the coroutine self-destroys via suspend_never
164 :
165 : // Forward parent cancellation
166 30 : new (cb_buf_) stop_cb_type(env->stop_token, stop_forwarder{&stop_src_});
167 30 : cb_active_ = true;
168 :
169 : // Start the inner op with our interposed stop_token
170 30 : inner_env_ = {
171 30 : env->executor, stop_src_.get_token(), env->frame_allocator};
172 60 : return inner_.await_suspend(h, &inner_env_);
173 60 : }
174 :
175 30 : decltype(auto) await_resume()
176 : {
177 : // Cancel whichever is still pending (idempotent)
178 30 : stop_src_.request_stop();
179 30 : destroy_parent_cb();
180 30 : return inner_.await_resume();
181 : }
182 :
183 90 : void destroy_parent_cb() noexcept
184 : {
185 90 : if (cb_active_)
186 : {
187 30 : std::launder(reinterpret_cast<stop_cb_type*>(cb_buf_))
188 30 : ->~stop_cb_type();
189 30 : cb_active_ = false;
190 : }
191 90 : }
192 : };
193 :
194 : } // namespace boost::corosio::detail
195 :
196 : #endif
|