Skip to content

Commit 3b42b47

Browse files
committed
Finish documentation pass
1 parent 3e97396 commit 3b42b47

File tree

4 files changed

+507
-197
lines changed

4 files changed

+507
-197
lines changed

doc/awaitable.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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.

doc/fieldparams.md

Lines changed: 6 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -144,80 +144,13 @@ report, accoring to the [spec](https://spec.graphql.org/October2021/#sec-Errors)
144144

145145
### Launch Policy
146146

147-
The `graphqlservice` library uses the `SelectionSetParams::launch` parameter to
148-
determine how it should handle async resolvers in the same selection set or
149-
elements in the same list. It is passed from the top-most `resolve`, `deliver`,
150-
or async `subscribe`/`unsubscribe` call. The `getField` accessors get a copy of
151-
this member in their `FieldParams` argument, and they may change their own
152-
behavior based on that, but they cannot alter the launch policy which
153-
`graphqlservice` uses for the resolvers themselves.
154-
155-
In previous versions, this was a `std::launch` enum value used with the
156-
`std::async` standard library function. In the latest version, this is a C++20
157-
`Awaitable`, specifically a type-erased type defined in `graphql::service::await_async`:
158-
```cpp
159-
// Type-erased awaitable.
160-
class await_async : public coro::suspend_always
161-
{
162-
private:
163-
struct Concept
164-
{
165-
virtual ~Concept() = default;
166-
167-
virtual bool await_ready() const = 0;
168-
virtual void await_suspend(coro::coroutine_handle<> h) const = 0;
169-
virtual void await_resume() const = 0;
170-
};
171-
...
172-
173-
public:
174-
// Type-erased explicit constructor for a custom awaitable.
175-
template <class T>
176-
explicit await_async(std::shared_ptr<T> pimpl)
177-
: _pimpl { std::make_shared<Model<T>>(std::move(pimpl)) }
178-
{
179-
}
180-
181-
// Default to immediate synchronous execution.
182-
await_async()
183-
: _pimpl { std::static_pointer_cast<Concept>(
184-
std::make_shared<Model<coro::suspend_never>>(std::make_shared<coro::suspend_never>())) }
185-
{
186-
}
187-
188-
// Implicitly convert a std::launch parameter used with std::async to an awaitable.
189-
await_async(std::launch launch)
190-
: _pimpl { ((launch & std::launch::async) == std::launch::async)
191-
? std::static_pointer_cast<Concept>(std::make_shared<Model<await_worker_thread>>(
192-
std::make_shared<await_worker_thread>()))
193-
: std::static_pointer_cast<Concept>(std::make_shared<Model<coro::suspend_never>>(
194-
std::make_shared<coro::suspend_never>())) }
195-
{
196-
}
197-
...
198-
};
199-
```
200-
For convenience, it will use `graphql::service::await_worker_thread` if you specify `std::launch::async`,
201-
which should have the same behavior as calling `std::async(std::launch::async, ...)` did before.
202-
203-
If you specify any other flags for `std::launch`, it does not honor them. It will use `coro::suspend_never`
204-
(an alias for `std::suspend_never` or `std::experimental::suspend_never`), which as the name suggests,
205-
continues executing the coroutine without suspending. In other words, `std::launch::deferred` will no
206-
longer defer execution as in previous versions, it will execute immediately.
207-
208-
There is also a default constructor which also uses `coro::suspend_never`, so that is the default
209-
behavior anywhere that `await_async` is default-initialized with `{}`.
210-
211-
Other than simplification, the big advantage this brings is in the type-erased template constructor.
212-
If you are using another C++20 library or thread/task pool with coroutine support, you can implement
213-
your own `Awaitable` for it and wrap that in `graphql::service::await_async`. It should automatically
214-
start parallelizing all of its resolvers using your custom scheduler, which can pause and resume the
215-
coroutine when and where it likes.
147+
See the [Awaitable](./awaitable.md) document for more information about
148+
`service::await_async`.
216149

217150
## Related Documents
218151

219152
1. The `getField` methods are discussed in more detail in [resolvers.md](./resolvers.md).
220-
2. Built-in and custom `directives` are discussed in [directives.md](./directives.md).
221-
3. Subscription resolvers get called up to 3 times depending on which
222-
`subscribe`/`unsubscribe` overrides you call. See [subscriptions.md](./subscriptions.md)
223-
for more details.
153+
2. Awaitable types are covered in [awaitable.md](./awaitable.md).
154+
3. Built-in and custom `directives` are discussed in [directives.md](./directives.md).
155+
4. Subscription resolvers may be called 2 extra times, inside of subscribe` and `unsubscribe`.
156+
See [subscriptions.md](./subscriptions.md) for more details.

0 commit comments

Comments
 (0)