Commit 514a49f5 authored by Jacob Lacouture's avatar Jacob Lacouture Committed by Facebook Github Bot

avoid throwing in thenValue

Summary:
AFAIK, thenValue should effectively NOP if the future has an exception.
Instead, it throws. C++ throw is expensive, as the stack_unwind process
acquires a futex. If the service is seeing lots of exceptions (for example
if it's overloaded and trying to shed load) throwing results in high lock
contention.

Reviewed By: yfeldblum

Differential Revision: D13088019

fbshipit-source-id: a4bd39d8196d8691ca3a2c21a4860024b366fdac
parent 3e8cb115
......@@ -53,6 +53,58 @@ std::shared_ptr<Timekeeper> getTimekeeperSingleton();
namespace futures {
namespace detail {
// InvokeResultWrapper and wrapInvoke enable wrapping a result value in its
// nearest Future-type counterpart capable of also carrying an exception.
// e.g.
// (semi)Future<T> -> (semi)Future<T> (no change)
// Try<T> -> Try<T> (no change)
// void -> Try<folly::Unit>
// T -> Try<T>
template <typename T>
struct InvokeResultWrapperBase {
template <typename F>
static T wrapResult(F fn) {
return T(fn());
}
static T wrapException(exception_wrapper&& e) {
return T(std::move(e));
}
};
template <typename T>
struct InvokeResultWrapper : InvokeResultWrapperBase<Try<T>> {};
template <typename T>
struct InvokeResultWrapper<Try<T>> : InvokeResultWrapperBase<Try<T>> {};
template <typename T>
struct InvokeResultWrapper<SemiFuture<T>>
: InvokeResultWrapperBase<SemiFuture<T>> {};
template <typename T>
struct InvokeResultWrapper<Future<T>> : InvokeResultWrapperBase<Future<T>> {};
template <>
struct InvokeResultWrapper<void> : InvokeResultWrapperBase<Try<Unit>> {
template <typename F>
static Try<Unit> wrapResult(F fn) {
fn();
return Try<Unit>(unit);
}
};
template <typename T, typename F>
auto wrapInvoke(folly::Try<T>&& t, F&& f) {
auto fn = [&]() {
return std::forward<F>(f)(
t.template get<
false,
typename futures::detail::valueCallableResult<T, F>::FirstArg>());
};
using FnResult = decltype(fn());
using Wrapper = InvokeResultWrapper<FnResult>;
if (t.hasException()) {
return Wrapper::wrapException(std::move(t).exception());
}
return Wrapper::wrapResult(fn);
}
// Guarantees that the stored functor is destructed before the stored promise
// may be fulfilled. Assumes the stored functor to be noexcept-destructible.
template <typename T, typename F>
......@@ -920,13 +972,10 @@ template <class T>
template <typename F>
SemiFuture<typename futures::detail::valueCallableResult<T, F>::value_type>
SemiFuture<T>::deferValue(F&& func) && {
return std::move(*this).defer([f = std::forward<F>(func)](
folly::Try<T>&& t) mutable {
return std::forward<F>(f)(
t.template get<
false,
typename futures::detail::valueCallableResult<T, F>::FirstArg>());
});
return std::move(*this).defer(
[f = std::forward<F>(func)](folly::Try<T>&& t) mutable {
return futures::detail::wrapInvoke(std::move(t), std::forward<F>(f));
});
}
template <class T>
......@@ -1134,10 +1183,7 @@ template <typename F>
Future<typename futures::detail::valueCallableResult<T, F>::value_type>
Future<T>::thenValue(F&& func) && {
auto lambdaFunc = [f = std::forward<F>(func)](folly::Try<T>&& t) mutable {
return std::forward<F>(f)(
t.template get<
false,
typename futures::detail::valueCallableResult<T, F>::FirstArg>());
return futures::detail::wrapInvoke(std::move(t), std::forward<F>(f));
};
using R = futures::detail::tryCallableResult<T, decltype(lambdaFunc)>;
return this->thenImplementation(std::move(lambdaFunc), R{});
......
......@@ -1355,6 +1355,79 @@ TEST(Future, ThenRecursion) {
EXPECT_EQ(42, recursion(&executor, 100000).getVia(&executor));
}
// We want to detect if the Try value is being dereferenced before being
// checked for validity. The only way to do that is with a custom Try impl.
struct NoThrowTestResult {};
namespace folly {
// Forward all methods except throwIfFailed().
template <>
class Try<NoThrowTestResult> : public Try<void> {
public:
using Try<void>::Try;
explicit Try(const NoThrowTestResult&) : Try<void>() {}
NoThrowTestResult value() const {
throwIfFailed();
return NoThrowTestResult();
}
NoThrowTestResult operator*() const {
return value();
}
// If the Try contains an exception, throws it
inline void throwIfFailed() const {
EXPECT_FALSE(this->hasException())
<< "throwIfFailed() should never have been invoked.";
Try<void>::throwIfFailed();
}
template <bool isTry, typename R>
typename std::enable_if<isTry, R>::type get() {
return std::forward<R>(*this);
}
template <bool isTry, typename R>
typename std::enable_if<!isTry, R>::type get() {
return std::forward<R>(value());
}
};
} // namespace folly
TEST(Future, NoThrow) {
// Test that the Futures implementation never invokes c++ throw, by
// accessing the value without first checking whether the value exists.
const std::string kErrorMessage = "NoThrow test";
// Test thenValue
{
Try<NoThrowTestResult> t =
Future<NoThrowTestResult>(std::runtime_error(kErrorMessage))
.thenValue([](NoThrowTestResult&& value) {
ADD_FAILURE() << "This code should be unreachable";
return std::move(value);
})
.getTry();
EXPECT_TRUE(t.hasException());
EXPECT_EQ(t.exception().get_exception()->what(), kErrorMessage);
}
// Test deferValue
{
Try<NoThrowTestResult> t =
SemiFuture<NoThrowTestResult>(std::runtime_error(kErrorMessage))
.deferValue([](NoThrowTestResult&& value) {
ADD_FAILURE() << "This code should be unreachable";
return std::move(value);
})
.via(&InlineExecutor::instance())
.getTry();
EXPECT_TRUE(t.hasException());
EXPECT_EQ(t.exception().get_exception()->what(), kErrorMessage);
}
}
#if FOLLY_FUTURE_USING_FIBER
TEST(Future, BatonWait) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment