StripeKit 0.1.1
Stripe integration toolkit for C++
Loading...
Searching...
No Matches
Async.h
1/** Async.h
2 *
3 * Coroutine task primitives used by StripeKit async methods.
4 *
5 * @author Hans de Ruiter
6 *
7 * @license See LICENSE.md for details.
8 */
9
10#pragma once
11
12#include <condition_variable>
13#include <coroutine>
14#include <exception>
15#include <mutex>
16#include <optional>
17#include <stdexcept>
18#include <utility>
19
20namespace stripekit {
21
22/**
23 * @ingroup async
24 *
25 * The result of an asynchronous StripeKit operation.
26 *
27 * Every `*_async` method returns one of these. `co_await` it to get the value:
28 *
29 * @code
30 * const stripekit::Customer customer = co_await stripe->customers().create_async(request);
31 * @endcode
32 *
33 * Outside a coroutine, result() blocks until the value is ready. That is
34 * convenient in command-line tools and tests, but must never be done on a
35 * thread that is running an event loop, since the work being waited on may
36 * need that loop to make progress.
37 *
38 * The task starts on the first `co_await` or result(), not on construction, so
39 * a task that is created and dropped never runs. Tasks are move-only, and each
40 * one produces its value once.
41 *
42 * @tparam TResult Type of value the operation produces.
43 */
44template <typename TResult>
45class Task {
46public:
47 /**
48 * Coroutine promise type. Used by the compiler; not called directly.
49 */
50 struct promise_type {
51 /**
52 * Creates the task object handed back to the caller.
53 *
54 * @return The task wrapping this coroutine.
55 */
57 return Task(std::coroutine_handle<promise_type>::from_promise(*this));
58 }
59
60 /**
61 * Suspends the coroutine at its start so the caller controls when it runs.
62 *
63 * @return An awaitable that always suspends.
64 */
65 std::suspend_always initial_suspend() noexcept {
66 return {};
67 }
68
69 /**
70 * Wakes anything waiting on this task once it finishes.
71 *
72 * @return An awaitable that notifies waiters and resumes the awaiting
73 * coroutine.
74 */
75 auto final_suspend() noexcept {
76 struct FinalAwaiter {
77 bool await_ready() const noexcept {
78 return false;
79 }
80
81 void await_suspend(std::coroutine_handle<promise_type> handle) const noexcept {
82 std::coroutine_handle<> continuation;
83 {
84 // Notify while still holding the lock: a waiter woken by
85 // notify_all() cannot reacquire the mutex, and therefore
86 // cannot destroy this coroutine's promise (mutex and
87 // condition_variable included), until this scope releases
88 // it below. Notifying after unlocking would let the
89 // waiter finish and destroy the promise concurrently with
90 // this notify_all() call, corrupting the heap.
91 std::lock_guard<std::mutex> lock(handle.promise().mutex);
92 handle.promise().completed = true;
93 continuation = handle.promise().continuation;
94 handle.promise().ready.notify_all();
95 }
96 if (continuation) {
97 continuation.resume();
98 }
99 }
100
101 void await_resume() const noexcept {}
102 };
103
104 return FinalAwaiter{};
105 }
106
107 /**
108 * Stores the value the coroutine returned.
109 *
110 * @param value The produced value.
111 */
112 void return_value(TResult value) {
113 result = std::move(value);
114 }
115
116 /**
117 * Captures an escaping exception so it can be rethrown to the waiter.
118 */
120 error = std::current_exception();
121 }
122
123 std::optional<TResult> result; /**< The produced value, once available. */
124 std::exception_ptr error; /**< The exception that escaped, if one did. */
125 std::mutex mutex; /**< Guards the fields below. */
126 std::condition_variable ready; /**< Signalled when the coroutine finishes. */
127 std::coroutine_handle<> continuation; /**< Coroutine waiting on this task. */
128 bool started = false; /**< True once the coroutine has been resumed. */
129 bool completed = false; /**< True once the coroutine has finished. */
130 };
131
132 /**
133 * Creates a task with no coroutine attached.
134 *
135 * Awaiting or querying an empty task throws.
136 */
137 Task() = default;
138
139 /**
140 * Destroys the task and its coroutine state.
141 */
143 if (handle) {
144 handle.destroy();
145 }
146 }
147
148 /**
149 * Adopts an existing coroutine handle.
150 *
151 * @param coroutine The coroutine this task takes ownership of.
152 */
153 explicit Task(std::coroutine_handle<promise_type> coroutine) : handle(coroutine) {}
154
155 Task(const Task&) = delete;
156 Task& operator=(const Task&) = delete;
157
158 /**
159 * Takes over another task's coroutine, leaving it empty.
160 *
161 * @param other The task to move from.
162 */
163 Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {}
164
165 /**
166 * Takes over another task's coroutine, leaving it empty.
167 *
168 * @param other The task to move from.
169 * @return This task.
170 */
171 Task& operator=(Task&& other) noexcept {
172 if (this != &other) {
173 if (handle) {
174 handle.destroy();
175 }
176 handle = std::exchange(other.handle, {});
177 }
178 return *this;
179 }
180
181 /**
182 * Reports whether the value is already available.
183 *
184 * @return True when the operation has finished, or the task is empty.
185 */
186 bool is_ready() const {
187 if (!handle) {
188 return true;
189 }
190 std::lock_guard<std::mutex> lock(handle.promise().mutex);
191 return handle.promise().completed;
192 }
193
194 /**
195 * Waits for the operation to finish and returns its value.
196 *
197 * Starts the operation if it has not started yet, then blocks the calling
198 * thread. Do not call this from an event loop thread: the work being waited
199 * on may need that loop to run, which would deadlock. Prefer `co_await`.
200 *
201 * @return The produced value, moved out of the task.
202 * @throws std::runtime_error The task is empty.
203 * @throws StripeApiException Or any other exception the operation threw.
204 */
205 TResult result() {
206 if (!handle) {
207 throw std::runtime_error("Task has no coroutine state");
208 }
209
210 start();
211
212 {
213 std::unique_lock<std::mutex> lock(handle.promise().mutex);
214 handle.promise().ready.wait(lock, [this]() { return handle.promise().completed; });
215 }
216
217 if (handle.promise().error) {
218 std::rethrow_exception(handle.promise().error);
219 }
220
221 return std::move(*handle.promise().result);
222 }
223
224 /**
225 * Makes the task awaitable, so `co_await` yields its value.
226 *
227 * Starts the operation if it has not started yet, suspends the awaiting
228 * coroutine, and resumes it with the value. An exception thrown by the
229 * operation is rethrown at the `co_await`.
230 *
231 * @return The awaiter used by the compiler.
232 */
233 auto operator co_await() {
234 struct Awaiter {
235 std::coroutine_handle<promise_type> task_handle;
236
237 bool await_ready() const noexcept {
238 return !task_handle || task_handle.promise().completed;
239 }
240
241 void await_suspend(std::coroutine_handle<> awaiting) const {
242 if (!task_handle) {
243 awaiting.resume();
244 return;
245 }
246
247 bool resume_task = false;
248 {
249 std::lock_guard<std::mutex> lock(task_handle.promise().mutex);
250 if (task_handle.promise().completed) {
251 awaiting.resume();
252 return;
253 } else {
254 task_handle.promise().continuation = awaiting;
255 resume_task = !task_handle.promise().started;
256 task_handle.promise().started = true;
257 }
258 }
259
260 if (resume_task) {
261 task_handle.resume();
262 }
263 }
264
265 TResult await_resume() {
266 if (!task_handle) {
267 throw std::runtime_error("Task has no coroutine state");
268 }
269
270 if (task_handle.promise().error) {
271 std::rethrow_exception(task_handle.promise().error);
272 }
273
274 return std::move(*task_handle.promise().result);
275 }
276 };
277
278 return Awaiter{ handle };
279 }
280
281private:
282 void start() {
283 bool should_resume = false;
284 {
285 std::lock_guard<std::mutex> lock(handle.promise().mutex);
286 should_resume = !handle.promise().started && !handle.promise().completed;
287 handle.promise().started = true;
288 }
289 if (should_resume) {
290 handle.resume();
291 }
292 }
293
294 std::coroutine_handle<promise_type> handle;
295};
296
297} // namespace stripekit
The result of an asynchronous StripeKit operation.
Definition Async.h:45
Task(std::coroutine_handle< promise_type > coroutine)
Adopts an existing coroutine handle.
Definition Async.h:153
bool is_ready() const
Reports whether the value is already available.
Definition Async.h:186
TResult result()
Waits for the operation to finish and returns its value.
Definition Async.h:205
Task & operator=(Task &&other) noexcept
Takes over another task's coroutine, leaving it empty.
Definition Async.h:171
Task()=default
Creates a task with no coroutine attached.
Task(Task &&other) noexcept
Takes over another task's coroutine, leaving it empty.
Definition Async.h:163
~Task()
Destroys the task and its coroutine state.
Definition Async.h:142
Async.h.
Definition Async.h:20
Coroutine promise type.
Definition Async.h:50
std::suspend_always initial_suspend() noexcept
Suspends the coroutine at its start so the caller controls when it runs.
Definition Async.h:65
std::exception_ptr error
The exception that escaped, if one did.
Definition Async.h:124
std::condition_variable ready
Signalled when the coroutine finishes.
Definition Async.h:126
std::coroutine_handle continuation
Coroutine waiting on this task.
Definition Async.h:127
auto final_suspend() noexcept
Wakes anything waiting on this task once it finishes.
Definition Async.h:75
std::mutex mutex
Guards the fields below.
Definition Async.h:125
void unhandled_exception()
Captures an escaping exception so it can be rethrown to the waiter.
Definition Async.h:119
Task get_return_object()
Creates the task object handed back to the caller.
Definition Async.h:56
std::optional< TResult > result
The produced value, once available.
Definition Async.h:123
void return_value(TResult value)
Stores the value the coroutine returned.
Definition Async.h:112
bool completed
True once the coroutine has finished.
Definition Async.h:129
bool started
True once the coroutine has been resumed.
Definition Async.h:128