Skip to content

Commit ef0d79a

Browse files
snarkmasterfacebook-github-bot
authored andcommitted
co_ready for unpacking result<T> values in async coros
Summary: Together with `co_await_result` & pre-existing `co_result`, this gives easy interop between sync `result<T>` coroutines and async ones like `Task<T>`. Reviewed By: ispeters Differential Revision: D71762048 fbshipit-source-id: abf9a797aa1565c5b80ac4b12825fb0c1d51ceac
1 parent 324a004 commit ef0d79a

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

folly/coro/Ready.h

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <folly/Executor.h>
20+
#include <folly/coro/Result.h>
21+
#include <folly/coro/WithAsyncStack.h>
22+
23+
/// Use `co_ready` to "await" synchronous coroutine types from inside async
24+
/// coroutines like `coro::Task`. For example:
25+
///
26+
/// result<int> getN();
27+
/// int n = co_await co_ready(getN());
28+
///
29+
/// Also see `co_await_result` (`AwaitResult.h`) and `co_result` (`Result.h`).
30+
///
31+
/// If you need to optimize away ALL exception throwing in **async** code,
32+
/// `co_ready` is not your top choice. In a `Task` coro:
33+
///
34+
/// auto v = co_await co_nothrow(asyncMayError()); // best practice
35+
/// auto v = co_await co_ready(co_await_result(asyncMayError())); // too long
36+
///
37+
/// However, when you are calling synchronous `result` functions, or need to
38+
/// efficiently handle **some** async errors, `co_ready` is your friend:
39+
///
40+
/// auto res = syncResultFn(); // or `co_await co_await_result(asyncFn())`
41+
/// if (auto* ex = get_exception<MyError>(res)) {
42+
/// /* handle ex */
43+
/// } else {
44+
/// auto v = co_await co_ready(std::move(res)); // propagate unhandled
45+
/// }
46+
///
47+
/// This pattern has a few good properties:
48+
/// - Easy error handling -- extracts the value from its argument, or
49+
/// short-circuit any error to current coro's awaiter.
50+
/// - Unlike `catch (const std::exception& ex)`, won't catch (and therefore
51+
/// break) cancellation.
52+
/// - The error path is MUCH more efficient (3-30 nanoseconds) than
53+
/// `value_or_throw()` (1 microsecond).
54+
///
55+
/// We don't support `co_await syncResultFn()` to avoids confusion about which
56+
/// parts of the code are sync vs async. The distinction is critical, since
57+
/// one must not hold non-coro mutexes across async suspend points.
58+
///
59+
/// Future:
60+
/// - Adding `std::ref` / `std::cref`, and possibly `folly::rref` variants of
61+
/// this (as in `result.h`) might improve performance in hot code.
62+
/// - The current implementation is `result`-only. If you have a need, it
63+
/// would be fine to add the analogous specialization for `Try`. Just be
64+
/// mindful of its two warts: empty state and empty `exception_wrapper`.
65+
66+
namespace folly::coro {
67+
68+
template <typename>
69+
class co_ready;
70+
71+
#if FOLLY_HAS_RESULT
72+
73+
template <typename T>
74+
class co_ready<result<T>> {
75+
private:
76+
result<T> res_;
77+
78+
public:
79+
explicit co_ready(result<T>&& res) : res_(std::move(res)) {}
80+
81+
bool await_ready() const noexcept { return res_.has_value(); }
82+
83+
auto await_resume() noexcept -> decltype(std::move(res_).value_or_throw()) {
84+
return std::move(res_).value_or_throw();
85+
}
86+
87+
template <typename Promise>
88+
auto await_suspend(
89+
std::coroutine_handle<Promise> awaitingCoroutine) noexcept {
90+
auto& promise = awaitingCoroutine.promise();
91+
// We have to use the legacy API because (1) `folly::coro` internals still
92+
// model cancellation as an exception, (2) to use `co_cancelled` here we'd
93+
// have to check `res_` for `OperationCancelled` which can cost 50-100ns+.
94+
auto awaiter = promise.yield_value(co_error(
95+
std::move(res_).non_value().get_legacy_error_or_cancellation()));
96+
return awaiter.await_suspend(awaitingCoroutine);
97+
}
98+
99+
friend auto co_viaIfAsync(
100+
const Executor::KeepAlive<>&, co_ready&& r) noexcept {
101+
return std::move(r);
102+
}
103+
104+
friend auto tag_invoke(cpo_t<co_withAsyncStack>, co_ready&& r) noexcept {
105+
return std::move(r);
106+
}
107+
};
108+
109+
template <typename T>
110+
co_ready(result<T>&&) -> co_ready<result<T>>;
111+
112+
#endif // FOLLY_HAS_RESULT
113+
114+
} // namespace folly::coro

folly/coro/test/ReadyTest.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <folly/coro/AwaitResult.h>
18+
#include <folly/coro/GtestHelpers.h>
19+
#include <folly/coro/Ready.h>
20+
#include <folly/coro/safe/NowTask.h>
21+
22+
#if FOLLY_HAS_RESULT
23+
24+
namespace folly::coro {
25+
26+
CO_TEST(ReadyTest, co_ready_of_error_result) {
27+
auto errorTaskFn = []() -> NowTask<> {
28+
result<> res = make_exception_wrapper<std::runtime_error>("foo");
29+
try {
30+
co_await co_ready(std::move(res));
31+
} catch (...) {
32+
} // propagates exception WITHOUT rethrowing
33+
LOG(FATAL) << "not reached";
34+
};
35+
auto res = co_await co_await_result(errorTaskFn());
36+
EXPECT_STREQ("foo", get_exception<std::runtime_error>(res)->what());
37+
}
38+
39+
CO_TEST(ReadyTest, co_ready_of_value_result) {
40+
auto valueTaskFn = []() -> NowTask<int> {
41+
result res = 1300;
42+
co_return 37 + co_await co_ready(std::move(res));
43+
};
44+
EXPECT_EQ(1337, co_await valueTaskFn());
45+
46+
// Use a move-only value to check we don't have extraneous copies
47+
auto moveValueTaskFn = []() -> NowTask<std::unique_ptr<int>> {
48+
result res = std::make_unique<int>(1300);
49+
auto np = co_await co_ready(std::move(res));
50+
*np += 37;
51+
co_return std::move(np);
52+
};
53+
EXPECT_EQ(1337, *(co_await moveValueTaskFn()));
54+
}
55+
56+
CO_TEST(ReadyTest, co_ready_of_void_result) {
57+
auto taskFn = [&]() -> NowTask<int> {
58+
result<> res;
59+
co_await co_ready(std::move(res));
60+
co_return 42;
61+
};
62+
EXPECT_EQ(42, co_await taskFn());
63+
}
64+
65+
CO_TEST(ReadyTest, co_ready_stopped) {
66+
auto taskFn = [&]() -> NowTask<int> {
67+
result<> res = stopped_result;
68+
co_await co_ready(std::move(res));
69+
co_return 42;
70+
};
71+
EXPECT_TRUE((co_await co_await_result(taskFn())).has_stopped());
72+
}
73+
74+
} // namespace folly::coro
75+
76+
#endif

0 commit comments

Comments
 (0)