morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
completion_awaiter.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <atomic>
5#include <core/async/Awaitable.hpp>
6#include <core/async/Cancellation.hpp>
7#include <core/async/ExecutorContext.hpp>
8#include <core/async/ParkedWork.hpp>
9#include <core/async/StopToken.hpp>
10#include <coroutine>
11#include <cstdint>
12#include <exception>
13#include <memory>
14#include <optional>
15#include <stdexcept>
16#include <utility>
17
18#include "../callback_scope.hpp"
19#include "../executor.hpp"
20
21/// @file
22/// @brief The awaiter behind `Completion<T>::operator co_await`.
23///
24/// A fragment of `completion.hpp` rather than of `coroutine.hpp`, so a
25/// `Completion<T>` is awaitable wherever it is visible. Specified in
26/// `docs/spec/core/coroutines.md`.
27
28namespace morph::async::detail {
29
30template <typename T>
31struct CompletionState;
32
33/// @brief The awaiter `Completion<T>::operator co_await() &&` returns.
34///
35/// The await is one more `then`/`onError` pair on the completion, holding only
36/// a heap `Shared` -- never the coroutine frame -- and gated by a
37/// `CallbackToken`. With a stoppable token on the awaiting promise it also
38/// registers a stop callback. Exactly one of *the completion's handler* and
39/// *the stop callback* moves `Shared::outcome` off `Pending`, and only that one
40/// resumes the coroutine. A stop that withdraws the await also stops the call
41/// the completion reports on, when that call carries a stop source.
42///
43/// The pair is attached on the completion's executor, which owns its handler
44/// lists: directly when the awaiting coroutine is running there, otherwise by
45/// a task posted there. So any coroutine may await any completion.
46///
47/// `await_suspend` touches only local copies of the shared pointers once it has
48/// published anything another thread could act on: from then on the coroutine
49/// may already have been resumed elsewhere, and the awaiter with its frame
50/// destroyed.
51/// @tparam T The completion's value type; copied out of the settled state.
52template <typename T>
53class CompletionAwaiter {
54 enum class Outcome : std::uint8_t { Pending, Settled, Cancelled };
55
56 struct Shared;
57
58 /// The stop callback's callable: forwards to `Shared::onStop`.
59 struct OnStop {
60 Shared* shared;
61 void operator()() const noexcept { shared->onStop(); }
62 };
63
64 struct Shared {
65 std::atomic<Outcome> outcome{Outcome::Pending};
66 /// Set once `await_suspend` has finished registering; before that a
67 /// stop only records itself and `await_suspend` answers it.
68 std::atomic<bool> armed{false};
69 /// Taken by whichever of `await_suspend` and `onStop` answers a stop.
70 std::atomic<bool> claimed{false};
71 std::optional<T> value;
72 std::exception_ptr error;
73 std::coroutine_handle<> continuation;
74 /// Where the coroutine was running when it suspended: resumed there.
75 /// Empty outside every executor's task.
76 ::core::async::ResumeTarget context;
77 ::morph::exec::IExecutor* fallback = nullptr;
78 CallbackScope scope;
79 std::optional<::core::async::StopCallback<OnStop>> stopCallback;
80
81 // seq_cst, as `armed` is: whoever decides and then reads `armed` must
82 // not miss the other side's store (a store-buffering pattern).
83 bool decide(Outcome outcome_) noexcept {
84 auto expected = Outcome::Pending;
85 return outcome.compare_exchange_strong(expected, outcome_, std::memory_order_seq_cst);
86 }
87
88 /// A settled completion, on the completion's executor: resumes in the
89 /// suspending context, or right here.
90 void resumeSettled() {
91 if (context) {
92 context.submit(::core::async::ParkedWork{.resume = continuation});
93 } else {
94 continuation.resume();
95 }
96 }
97
98 /// A stop, on whichever thread requested it: resumes through the
99 /// suspending context, or on the completion's executor. Not inline
100 /// here, because the requesting thread is not the coroutine's; the
101 /// context itself may resume inline (a Task handler's resumer whose
102 /// strands are closed does).
103 void resumeCancelled() {
104 if (context) {
105 context.submit(::core::async::ParkedWork{.resume = continuation});
106 } else {
107 fallback->post([handle = continuation] { handle.resume(); });
108 }
109 }
110
111 void onStop() noexcept {
112 if (!decide(Outcome::Cancelled)) {
113 return;
114 }
115 scope.requestStop();
116 if (armed.load(std::memory_order_seq_cst) && !claimed.exchange(true, std::memory_order_acq_rel)) {
117 try {
118 resumeCancelled();
119 } catch (...) { // NOLINT(bugprone-empty-catch): a stop callback must not throw
120 // A queue that cannot accept one more closure leaves nothing to
121 // resume through; the coroutine stays suspended until its
122 // owner destroys it.
123 }
124 }
125 }
126 };
127
128public:
129 /// @param state The completion's state; consumed. Null for an empty completion.
130 explicit CompletionAwaiter(std::shared_ptr<CompletionState<T>> state)
131 : _state{std::move(state)}, _shared{std::make_shared<Shared>()} {}
132
133 /// Withdraws the await if the frame is destroyed while suspended, so neither
134 /// a later settlement nor a later stop resumes a frame that is gone.
135 ~CompletionAwaiter() {
136 _shared->stopCallback.reset();
137 _shared->scope.requestStop();
138 }
139
140 CompletionAwaiter(const CompletionAwaiter&) = delete;
141 CompletionAwaiter& operator=(const CompletionAwaiter&) = delete;
142 CompletionAwaiter(CompletionAwaiter&&) = delete;
143 CompletionAwaiter& operator=(CompletionAwaiter&&) = delete;
144
145 /// @return Always false: every decision is `await_suspend`'s, so this stays
146 /// a constant (see core-cpp's `awaitReadyIsConstantFalse`).
147 [[nodiscard]] constexpr bool await_ready() const noexcept { return false; }
148
149 /// @tparam Promise The awaiting coroutine's promise type.
150 /// @param awaiting The coroutine performing the `co_await`.
151 /// @return False to continue at once (an unusable completion, or a stop
152 /// already requested); true once the await is attached.
153 template <typename Promise>
154 // NOLINTNEXTLINE(readability-function-cognitive-complexity): the whole attach decision lives here by design (see await_ready); each branch is one way the await ends or suspends.
155 bool await_suspend(std::coroutine_handle<Promise> awaiting) {
156 auto shared = _shared;
157 auto state = _state;
158 if (state == nullptr || state->cbExec == nullptr) {
159 shared->error = std::make_exception_ptr(std::logic_error{
160 state == nullptr ? "co_await on an empty morph::async::Completion (default-constructed or moved-from)"
161 : "co_await on a morph::async::Completion with no callback executor to resume on"});
162 shared->outcome.store(Outcome::Settled);
163 return false;
164 }
165 shared->continuation = awaiting;
166 shared->context = ::core::async::ResumeTarget::current();
167 shared->fallback = state->cbExec;
168
169 if constexpr (::core::async::HasStopToken<Promise>) {
170 ::core::async::StopToken const token = awaiting.promise().stopToken();
171 if (token.stop_requested()) {
172 shared->outcome.store(Outcome::Cancelled);
173 stopUnattached(state, token);
174 return false;
175 }
176 if (token.stop_possible()) {
177 shared->stopCallback.emplace(token, OnStop{shared.get()});
178 shared->armed.store(true, std::memory_order_seq_cst);
179 if (shared->outcome.load(std::memory_order_seq_cst) == Outcome::Cancelled) {
180 // A stop landed while registering. Whoever claims answers
181 // it; if onStop claimed first it is resuming us already.
182 stopUnattached(state, token);
183 return shared->claimed.exchange(true, std::memory_order_acq_rel);
184 }
185 }
186 }
187
188 try {
189 if (::morph::exec::runningOn(*state->cbExec)) {
190 attach(*state, shared);
191 } else {
192 // Attaching is the owner's: post it there. The executor's
193 // queue then orders it with the settle's delivery, and the
194 // handlers it attaches resume the coroutine as above.
195 state->cbExec->post([state, shared] {
196 try {
197 attach(*state, shared);
198 } catch (...) {
199 if (shared->decide(Outcome::Settled)) {
200 shared->error = std::current_exception();
201 shared->resumeSettled();
202 }
203 }
204 });
205 }
206 } catch (...) {
207 // The throw resumes the coroutine, so nothing else may. If a stop
208 // has claimed the await it is resuming the coroutine already, and
209 // the frame is no longer this call's to throw into: that stop's
210 // OperationCancelled is what the coroutine sees.
211 if (shared->claimed.exchange(true, std::memory_order_acq_rel)) {
212 shared->scope.requestStop();
213 return true;
214 }
215 // Likewise if the handler attached before the throw has already
216 // settled the await: it resumed the coroutine.
217 if (!shared->decide(Outcome::Settled) && shared->outcome.load() == Outcome::Settled) {
218 shared->scope.requestStop();
219 return true;
220 }
221 // Still suspended, and decided: withdraw the stop callback (waiting
222 // out one that is running) and the handler attached before the throw.
223 shared->stopCallback.reset();
224 shared->scope.requestStop();
225 throw;
226 }
227 return true;
228 }
229
230 /// @return The settled value.
231 /// @throws core::async::OperationCancelled if a stop withdrew the await.
232 /// @throws Whatever the completion was rejected with.
233 /// @throws std::logic_error for an empty completion or one with no executor.
234 T await_resume() {
235 _shared->stopCallback.reset();
236 if (_shared->outcome.load() == Outcome::Cancelled) {
237 throw ::core::async::OperationCancelled{};
238 }
239 if (_shared->error) {
240 std::rethrow_exception(_shared->error);
241 }
242 return std::move(*_shared->value);
243 }
244
245private:
246 /// Attaches the await's handler pair to @p state; runs on its executor.
247 ///
248 /// The await owns the call it waits on, as a scope-gated continuation
249 /// does: the scope's stop -- a stop that withdraws the await, or the frame
250 /// destroyed while suspended -- is linked to the call's stop source, so
251 /// withdrawing the await also asks the call to stop. Linked after the
252 /// scope has stopped, it stops the call at once.
253 static void attach(CompletionState<T>& state, const std::shared_ptr<Shared>& shared) {
254 auto const token = shared->scope.token();
255 state.linkStop(token.stopToken());
256 state.attachThen([token, shared](const T& settled) {
257 if (token.active() && shared->decide(Outcome::Settled)) {
258 shared->value.emplace(settled);
259 shared->resumeSettled();
260 }
261 });
262 state.attachOnError([token, shared](std::exception_ptr error) {
263 if (token.active() && shared->decide(Outcome::Settled)) {
264 shared->error = std::move(error);
265 shared->resumeSettled();
266 }
267 });
268 }
269
270 /// Stops the call @p state reports on, for an await a stop withdrew before
271 /// it attached, so no link carries that stop to the call. @p stopped,
272 /// already stopped, is linked on the completion's executor, which stops the
273 /// call there. Best effort: an executor that refuses the task leaves the
274 /// call running; the coroutine resumes with the cancellation either way.
275 static void stopUnattached(const std::shared_ptr<CompletionState<T>>& state,
276 const ::core::async::StopToken& stopped) noexcept {
277 try {
278 if (::morph::exec::runningOn(*state->cbExec)) {
279 state->linkStop(stopped);
280 return;
281 }
282 state->cbExec->post([state, stopped] { state->linkStop(stopped); });
283 } catch (...) { // NOLINT(bugprone-empty-catch): the await is answered already
284 }
285 }
286
287 std::shared_ptr<CompletionState<T>> _state;
288 std::shared_ptr<Shared> _shared;
289};
290
291} // namespace morph::async::detail