|
| 1 | +# Awaitable |
| 2 | + |
| 3 | +## Launch Policy |
| 4 | + |
| 5 | +In previous versions, this was a `std::launch` enum value used with the |
| 6 | +`std::async` standard library function. Now, this is a C++20 `Awaitable`, |
| 7 | +specifically a type-erased `graphql::service::await_async` class in |
| 8 | +[GraphQLService.h](../include/graphqlservice/GraphQLService.h): |
| 9 | +```cpp |
| 10 | +// Type-erased awaitable. |
| 11 | +class await_async : public coro::suspend_always |
| 12 | +{ |
| 13 | +private: |
| 14 | + struct Concept |
| 15 | + { |
| 16 | + virtual ~Concept() = default; |
| 17 | + |
| 18 | + virtual bool await_ready() const = 0; |
| 19 | + virtual void await_suspend(coro::coroutine_handle<> h) const = 0; |
| 20 | + virtual void await_resume() const = 0; |
| 21 | + }; |
| 22 | +... |
| 23 | + |
| 24 | +public: |
| 25 | + // Type-erased explicit constructor for a custom awaitable. |
| 26 | + template <class T> |
| 27 | + explicit await_async(std::shared_ptr<T> pimpl) |
| 28 | + : _pimpl { std::make_shared<Model<T>>(std::move(pimpl)) } |
| 29 | + { |
| 30 | + } |
| 31 | + |
| 32 | + // Default to immediate synchronous execution. |
| 33 | + await_async() |
| 34 | + : _pimpl { std::static_pointer_cast<Concept>( |
| 35 | + std::make_shared<Model<coro::suspend_never>>(std::make_shared<coro::suspend_never>())) } |
| 36 | + { |
| 37 | + } |
| 38 | + |
| 39 | + // Implicitly convert a std::launch parameter used with std::async to an awaitable. |
| 40 | + await_async(std::launch launch) |
| 41 | + : _pimpl { ((launch & std::launch::async) == std::launch::async) |
| 42 | + ? std::static_pointer_cast<Concept>(std::make_shared<Model<await_worker_thread>>( |
| 43 | + std::make_shared<await_worker_thread>())) |
| 44 | + : std::static_pointer_cast<Concept>(std::make_shared<Model<coro::suspend_never>>( |
| 45 | + std::make_shared<coro::suspend_never>())) } |
| 46 | + { |
| 47 | + } |
| 48 | +... |
| 49 | +}; |
| 50 | +``` |
| 51 | +For convenience, it will use `graphql::service::await_worker_thread` if you specify `std::launch::async`, |
| 52 | +which should have the same behavior as calling `std::async(std::launch::async, ...)` did before. |
| 53 | +
|
| 54 | +If you specify any other flags for `std::launch`, it does not honor them. It will use `coro::suspend_never` |
| 55 | +(an alias for `std::suspend_never` or `std::experimental::suspend_never`), which as the name suggests, |
| 56 | +continues executing the coroutine without suspending. In other words, `std::launch::deferred` will no |
| 57 | +longer defer execution as in previous versions, it will execute immediately. |
| 58 | +
|
| 59 | +There is also a default constructor which also uses `coro::suspend_never`, so that is the default |
| 60 | +behavior anywhere that `await_async` is default-initialized with `{}`. |
| 61 | +
|
| 62 | +Other than simplification, the big advantage this brings is in the type-erased template constructor. |
| 63 | +If you are using another C++20 library or thread/task pool with coroutine support, you can implement |
| 64 | +your own `Awaitable` for it and wrap that in `graphql::service::await_async`. It should automatically |
| 65 | +start parallelizing all of its resolvers using your custom scheduler, which can pause and resume the |
| 66 | +coroutine when and where it likes. |
| 67 | +
|
| 68 | +## Awaitable Results |
| 69 | +
|
| 70 | +Many APIs which used to return some sort of `std::future` now return an alias for |
| 71 | +`graphql::internal::Awaitable<...>`. This template is defined in [Awaitable.h](../include/graphqlservice/internal/Awaitable.h): |
| 72 | +```cpp |
| 73 | +template <typename T> |
| 74 | +class Awaitable |
| 75 | +{ |
| 76 | +public: |
| 77 | + Awaitable(std::future<T> value) |
| 78 | + : _value { std::move(value) } |
| 79 | + { |
| 80 | + } |
| 81 | +
|
| 82 | + T get() |
| 83 | + { |
| 84 | + return _value.get(); |
| 85 | + } |
| 86 | +
|
| 87 | + struct promise_type |
| 88 | + { |
| 89 | + Awaitable get_return_object() noexcept |
| 90 | + { |
| 91 | + return { _promise.get_future() }; |
| 92 | + } |
| 93 | +
|
| 94 | + ... |
| 95 | +
|
| 96 | + void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>) |
| 97 | + { |
| 98 | + _promise.set_value(std::move(value)); |
| 99 | + } |
| 100 | +
|
| 101 | + ... |
| 102 | +
|
| 103 | + private: |
| 104 | + std::promise<T> _promise; |
| 105 | +
|
| 106 | + }; |
| 107 | +
|
| 108 | + constexpr bool await_ready() const noexcept |
| 109 | + { |
| 110 | + return true; |
| 111 | + } |
| 112 | +
|
| 113 | + void await_suspend(coro::coroutine_handle<> h) const |
| 114 | + { |
| 115 | + h.resume(); |
| 116 | + } |
| 117 | +
|
| 118 | + T await_resume() |
| 119 | + { |
| 120 | + return _value.get(); |
| 121 | + } |
| 122 | +
|
| 123 | +private: |
| 124 | + std::future<T> _value; |
| 125 | +}; |
| 126 | +``` |
| 127 | + |
| 128 | +The key details are that it implements the required `promise_type` and `await_` methods so |
| 129 | +that you can turn any `co_return` statement into a `std::future<T>`, and it can either |
| 130 | +`co_await` for that `std::future<T>` from a coroutine, or call `T get()` to block a regular |
| 131 | +function until it completes. |
| 132 | + |
| 133 | +## AwaitableScalar and AwaitableObject |
| 134 | + |
| 135 | +In previous versions, `service::FieldResult<T>` created an abstraction over return types `T` and |
| 136 | +`std::future<T>`, when returning from a field getter you could return either and it would |
| 137 | +implicitly convert that to a `service::FieldResult<T>` which looked and acted like a |
| 138 | +`std::future<T>`. |
| 139 | + |
| 140 | +Now, `service::FieldResult<T>` is replaced with `service::AwaitableScalar` for `scalar` type |
| 141 | +fields without a selection set of sub-fields, or `service::AwaitableObject` for `object` |
| 142 | +type fields which must have a selection set of sub-fields. The difference between |
| 143 | +`service::AwaitableScalar` and `service::AwaitableObject` is that `scalar` type fields can |
| 144 | +also return `std::shared_ptr<const response::Value>` directly, which bypasses all of the |
| 145 | +conversion logic in `service::ModifiedResult` and just validates that the shape of the |
| 146 | +`response::Value` matches the `scalar` type with all of its modifiers. These are both defined |
| 147 | +in [GraphQLService.h](../include/graphqlservice/GraphQLService.h): |
| 148 | +```cpp |
| 149 | +// Field accessors may return either a result of T, an awaitable of T, or a std::future<T>, so at |
| 150 | +// runtime the implementer may choose to return by value or defer/parallelize expensive operations |
| 151 | +// by returning an async future or an awaitable coroutine. |
| 152 | +// |
| 153 | +// If the overhead of conversion to response::Value is too expensive, scalar type field accessors |
| 154 | +// can store and return a std::shared_ptr<const response::Value> directly. |
| 155 | +template <typename T> |
| 156 | +class AwaitableScalar |
| 157 | +{ |
| 158 | +public: |
| 159 | + template <typename U> |
| 160 | + AwaitableScalar(U&& value) |
| 161 | + : _value { std::forward<U>(value) } |
| 162 | + { |
| 163 | + } |
| 164 | + |
| 165 | + struct promise_type |
| 166 | + { |
| 167 | + AwaitableScalar<T> get_return_object() noexcept |
| 168 | + { |
| 169 | + return { _promise.get_future() }; |
| 170 | + } |
| 171 | + |
| 172 | + ... |
| 173 | + |
| 174 | + void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) |
| 175 | + { |
| 176 | + _promise.set_value(value); |
| 177 | + } |
| 178 | + |
| 179 | + void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>) |
| 180 | + { |
| 181 | + _promise.set_value(std::move(value)); |
| 182 | + } |
| 183 | + |
| 184 | + ... |
| 185 | + |
| 186 | + private: |
| 187 | + std::promise<T> _promise; |
| 188 | + }; |
| 189 | + |
| 190 | + bool await_ready() const noexcept { ... } |
| 191 | + |
| 192 | + void await_suspend(coro::coroutine_handle<> h) const { ... } |
| 193 | + |
| 194 | + T await_resume() |
| 195 | + { |
| 196 | + ... // Throws std::logic_error("Cannot await std::shared_ptr<const response::Value>") if called with that alternative |
| 197 | + } |
| 198 | + |
| 199 | + std::shared_ptr<const response::Value> get_value() noexcept |
| 200 | + { |
| 201 | + ... // Returns an empty std::shared_ptr if called with a different alternative |
| 202 | + } |
| 203 | + |
| 204 | +private: |
| 205 | + std::variant<T, std::future<T>, std::shared_ptr<const response::Value>> _value; |
| 206 | +}; |
| 207 | + |
| 208 | +// Field accessors may return either a result of T, an awaitable of T, or a std::future<T>, so at |
| 209 | +// runtime the implementer may choose to return by value or defer/parallelize expensive operations |
| 210 | +// by returning an async future or an awaitable coroutine. |
| 211 | +template <typename T> |
| 212 | +class AwaitableObject |
| 213 | +{ |
| 214 | +public: |
| 215 | + template <typename U> |
| 216 | + AwaitableObject(U&& value) |
| 217 | + : _value { std::forward<U>(value) } |
| 218 | + { |
| 219 | + } |
| 220 | + |
| 221 | + struct promise_type |
| 222 | + { |
| 223 | + AwaitableObject<T> get_return_object() noexcept |
| 224 | + { |
| 225 | + return { _promise.get_future() }; |
| 226 | + } |
| 227 | + |
| 228 | + ... |
| 229 | + |
| 230 | + void return_value(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) |
| 231 | + { |
| 232 | + _promise.set_value(value); |
| 233 | + } |
| 234 | + |
| 235 | + void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>) |
| 236 | + { |
| 237 | + _promise.set_value(std::move(value)); |
| 238 | + } |
| 239 | + |
| 240 | + ... |
| 241 | + |
| 242 | + private: |
| 243 | + std::promise<T> _promise; |
| 244 | + }; |
| 245 | + |
| 246 | + bool await_ready() const noexcept { ... } |
| 247 | + |
| 248 | + void await_suspend(coro::coroutine_handle<> h) const { ... } |
| 249 | + |
| 250 | + T await_resume() { ... } |
| 251 | + |
| 252 | +private: |
| 253 | + std::variant<T, std::future<T>> _value; |
| 254 | +}; |
| 255 | +``` |
| 256 | + |
| 257 | +These types both add a `promise_type` for `T`, but coroutines need their own return type to do that. |
| 258 | +Making `service::AwaitableScalar<T>` or `service::AwaitableObject<T>` the return type of a field |
| 259 | +getter means you can turn it into a coroutine by just replacing `return` with `co_return`, and |
| 260 | +potentially start to `co_await` other awaitables and coroutines. |
| 261 | + |
| 262 | +Type-erasure made it so you do not need to use a special return type, the type-erased |
| 263 | +`Object::Model<T>` type just needs to be able to pass the return result from your field |
| 264 | +getter into a constructor for one of these return types. So if you want to implement |
| 265 | +your field getters as coroutines, you should still wrap the return type in |
| 266 | +`service::AwaitableScalar<T>` or `service::AwaitableObject<T>`. Otherwise, you can remove |
| 267 | +the template wrapper from all of your field getters. |
0 commit comments