Skip to content

Commit 324a004

Browse files
snarkmasterfacebook-github-bot
authored andcommitted
co_await_result -- a result<T> analog of co_awaitTry
Summary: See docblock. The goal is to lower the barrier for adopting `result` in new code. Reviewed By: ispeters Differential Revision: D71752310 fbshipit-source-id: bf588066387760eecb65f075b2fec3bcc5864ed8
1 parent 707c9aa commit 324a004

File tree

4 files changed

+246
-5
lines changed

4 files changed

+246
-5
lines changed

folly/coro/AwaitResult.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/coro/ViaIfAsync.h>
20+
#include <folly/result/try.h>
21+
22+
#if FOLLY_HAS_RESULT
23+
24+
namespace folly::coro {
25+
26+
/// `co_await_result` is the `result<T>` analog of the older `co_awaitTry`.
27+
///
28+
/// In a `folly::coro` async coroutine, use `co_await_result` like so:
29+
///
30+
/// result<int> res = co_await co_await_result(taskReturningInt());
31+
/// if (auto* ex = get_exception<MyError>(res)) {
32+
/// /* handle ex */
33+
/// } else {
34+
/// sum += co_await co_ready(res); // efficiently propagate unhandled error
35+
/// }
36+
///
37+
/// Contrast that with related async coro vocabulary:
38+
/// - `co_yield co_result(r)` from `Result.h` -- propagate `result<T>` or
39+
/// `Try<T>` to the awaiter of the current coro.
40+
/// - `auto& v = co_await co_ready(r)` from `Ready.h` -- given a `result<T>`,
41+
/// unpack the value, or propagate any error to our awaiter.
42+
///
43+
/// The purpose of `co_await_result` is to handle errors from a child task via
44+
/// `result<T>`, rather than through `try {} catch {}`. Some reasons to do so:
45+
/// - Your error-handling APIs (logging, retry, etc) use `result<T>`.
46+
/// - You wish to avoid the ~microsecond cost of thrown exceptions,
47+
/// applicable only when your error path is hot, and the child uses
48+
/// `co_yield` instead of `throw` to propagate exceptions.
49+
50+
namespace detail {
51+
52+
// This reuses the `await_resume_try` machinery in the hope that the compiler
53+
// will be able to optimize away the `Try` -> `result` conversion. In some
54+
// cases, this may incur an extra move-copy, so if this ends up being hot,
55+
// adding a specialized `await_resume_result` interface may be justified.
56+
template <typename Awaitable>
57+
class ResultAwaiter {
58+
private:
59+
static_assert(is_awaitable_try<Awaitable&&>);
60+
61+
using Awaiter = awaiter_type_t<Awaitable>;
62+
Awaiter awaiter_;
63+
64+
public:
65+
explicit ResultAwaiter(Awaitable&& awaiter)
66+
: awaiter_(get_awaiter(static_cast<Awaitable&&>(awaiter))) {}
67+
68+
// clang-format off
69+
auto await_ready() FOLLY_DETAIL_FORWARD_BODY(awaiter_.await_ready())
70+
71+
template <typename Promise>
72+
auto await_suspend(coroutine_handle<Promise> coro)
73+
FOLLY_DETAIL_FORWARD_BODY(awaiter_.await_suspend(coro))
74+
75+
auto await_resume()
76+
FOLLY_DETAIL_FORWARD_BODY(try_to_result(awaiter_.await_resume_try()))
77+
// clang-format on
78+
};
79+
80+
template <typename T>
81+
class [[FOLLY_ATTR_CLANG_CORO_AWAIT_ELIDABLE]] ResultAwaitable
82+
: public CommutativeWrapperAwaitable<ResultAwaitable, T> {
83+
public:
84+
using CommutativeWrapperAwaitable<ResultAwaitable, T>::
85+
CommutativeWrapperAwaitable;
86+
87+
template <
88+
typename Self,
89+
std::enable_if_t<
90+
std::is_same_v<remove_cvref_t<Self>, ResultAwaitable>,
91+
int> = 0,
92+
typename T2 = like_t<Self, T>,
93+
std::enable_if_t<is_awaitable_v<T2>, int> = 0>
94+
friend ResultAwaiter<T2> operator co_await(Self && self) {
95+
return ResultAwaiter<T2>{static_cast<Self&&>(self).inner_};
96+
}
97+
98+
using folly_private_noexcept_awaitable_t = std::true_type;
99+
};
100+
101+
} // namespace detail
102+
103+
// IMPORTANT: If you need an `Awaitable&&` overload, you must bifurcate this
104+
// API on `must_await_immediately_v`, see `co_awaitTry` for an example.
105+
template <typename Awaitable>
106+
detail::ResultAwaitable<Awaitable> co_await_result(
107+
[[FOLLY_ATTR_CLANG_CORO_AWAIT_ELIDABLE_ARGUMENT]] Awaitable awaitable) {
108+
return detail::ResultAwaitable<Awaitable>{
109+
mustAwaitImmediatelyUnsafeMover(std::move(awaitable))()};
110+
}
111+
112+
} // namespace folly::coro
113+
114+
#endif

folly/coro/Result.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <folly/ExceptionWrapper.h>
2323
#include <folly/OperationCancelled.h>
2424
#include <folly/Try.h>
25+
#include <folly/result/result.h>
2526

2627
namespace folly {
2728
namespace coro {
@@ -56,6 +57,14 @@ class co_result final {
5657
assert(!result_.hasException() || result_.exception());
5758
}
5859

60+
#if FOLLY_HAS_RESULT
61+
// Covered in `AwaitResultTest.cpp`, unlike the rest of this file, which is
62+
// covered in `TaskTest.cpp`.
63+
explicit co_result(folly::result<T>&& result) noexcept(
64+
std::is_nothrow_move_constructible<T>::value)
65+
: co_result(result_to_try(std::move(result))) {}
66+
#endif
67+
5968
const Try<T>& result() const { return result_; }
6069

6170
Try<T>& result() { return result_; }
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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/Result.h>
20+
#include <folly/coro/safe/NowTask.h>
21+
22+
/// Besides `co_await_result`, this test also covers the `folly::result<T>`
23+
/// integration with `coro::co_result` from `coro/Result.h`.
24+
25+
#if FOLLY_HAS_RESULT
26+
27+
namespace folly::coro {
28+
29+
CO_TEST(AwaitResultTest, co_await_result_of_error) {
30+
auto voidErrorTask = []() -> NowTask<void> {
31+
co_yield co_error(std::runtime_error("foo"));
32+
};
33+
{ // Capture the error
34+
auto res = co_await co_await_result(voidErrorTask());
35+
EXPECT_STREQ("foo", get_exception<std::runtime_error>(res)->what());
36+
}
37+
{ // Also test `coro::co_result` integration
38+
auto res = co_await co_await_result([&]() -> NowTask<void> {
39+
co_yield co_result(co_await co_await_result(voidErrorTask()));
40+
}());
41+
EXPECT_STREQ("foo", get_exception<std::runtime_error>(res)->what());
42+
}
43+
}
44+
45+
CO_TEST(AwaitResultTest, co_await_result_of_value) {
46+
// Return a move-only thing to make sure we don't copy
47+
auto valueTask = []() -> NowTask<std::unique_ptr<int>> {
48+
co_return std::make_unique<int>(1337);
49+
};
50+
{ // Capture the value
51+
auto res = co_await co_await_result(valueTask());
52+
// Real code should use `co_await co_ready(res)` to unpack!
53+
EXPECT_EQ(1337, *res.value_or_throw());
54+
}
55+
{ // Also test `coro::co_result` integration
56+
auto res = co_await co_await_result([&]() -> NowTask<std::unique_ptr<int>> {
57+
co_yield co_result(co_await co_await_result(valueTask()));
58+
}());
59+
EXPECT_EQ(1337, *res.value_or_throw());
60+
}
61+
}
62+
63+
CO_TEST(AwaitResultTest, co_await_result_of_void) {
64+
int numAwaited = 0;
65+
auto voidTask = [&]() -> NowTask<void> {
66+
++numAwaited;
67+
co_return;
68+
};
69+
{ // Capturing a "value" completion
70+
auto res = co_await co_await_result(voidTask());
71+
static_assert(std::is_same_v<decltype(res), result<>>);
72+
EXPECT_TRUE(res.has_value());
73+
EXPECT_EQ(1, numAwaited);
74+
}
75+
{ // Also test `coro::co_result` integration
76+
auto res = co_await co_await_result([&]() -> NowTask<void> {
77+
co_yield co_result(co_await co_await_result(voidTask()));
78+
}());
79+
static_assert(std::is_same_v<decltype(res), result<>>);
80+
EXPECT_TRUE(res.has_value());
81+
EXPECT_EQ(2, numAwaited);
82+
}
83+
}
84+
85+
CO_TEST(AwaitResultTest, co_await_result_stopped) {
86+
auto stoppedTask = [&]() -> NowTask<void> {
87+
co_yield co_cancelled;
88+
LOG(FATAL) << "not reached";
89+
};
90+
{ // Capturing a "stopped" completion
91+
auto res = co_await co_await_result(stoppedTask());
92+
EXPECT_TRUE(res.has_stopped());
93+
}
94+
{ // Also test `coro::co_result` integration
95+
auto res = co_await co_await_result([&]() -> NowTask<void> {
96+
co_yield co_result(co_await co_await_result(stoppedTask()));
97+
LOG(FATAL) << "not reached";
98+
}());
99+
EXPECT_TRUE(res.has_stopped());
100+
}
101+
}
102+
103+
struct ThrowingMove {
104+
ThrowingMove(const ThrowingMove&) = default;
105+
ThrowingMove& operator=(const ThrowingMove&) = default;
106+
ThrowingMove(ThrowingMove&&) {}
107+
ThrowingMove& operator=(ThrowingMove&&) { return *this; }
108+
};
109+
struct NothrowMove {};
110+
111+
template <typename T>
112+
using TestAwaiter = detail::ResultAwaiter<TaskWithExecutor<T>>;
113+
114+
// While it's unclear if anything cares about `await_resume()` `noexcept`
115+
// discipline, it's easy enough to check.
116+
static_assert(noexcept(FOLLY_DECLVAL(TestAwaiter<int>).await_resume()));
117+
static_assert(noexcept(FOLLY_DECLVAL(TestAwaiter<NothrowMove>).await_resume()));
118+
static_assert(
119+
!noexcept(FOLLY_DECLVAL(TestAwaiter<ThrowingMove>).await_resume()));
120+
121+
} // namespace folly::coro
122+
123+
#endif

folly/coro/test/NoexceptTest.cpp

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,6 @@ CO_TEST(NoexceptTest, AsNoexceptNowTask) {
260260
co_await checkAsNoexcept<NowTask<void>>();
261261
}
262262

263-
// XXX test AsNoexcept more -- generally need to cover most of the compositional
264-
// properties above :(
265-
// - cover WITH and WITHOUT executor
266-
// - crib more from `checkFatalOnThrow` and `NowTaskIsImmediate` above
267-
268263
} // namespace folly::coro
269264

270265
#endif

0 commit comments

Comments
 (0)