Align the cancel behaviour of result to the one used in the unhandled exception handler

* Introduce cancellation_result to represent a cancelled async task
* Add cancellation unit tests
* This doesn't allow cancellation of continuables, it is meant
  for treating the special state action canceled on the receiver side.
  Cancellation of a chain is still up to the user.
This commit is contained in:
Denis Blank 2020-04-03 20:54:15 +02:00
parent c8c4325b5b
commit 957d3fa375
7 changed files with 156 additions and 36 deletions

View File

@ -180,6 +180,26 @@ public:
data_ = nullptr; data_ = nullptr;
} }
/// Resolves the continuation with the cancellation token which is represented
/// by a default constructed exception_t.
///
/// \throws This method never throws an exception.
///
/// \attention This method may only be called once,
/// when the promise is valid operator bool() returns true.
/// Calling this method will invalidate the promise such that
/// subsequent calls to operator bool() will return false.
/// This behaviour is only consistent in promise_base and
/// non type erased promises may behave differently.
/// Invoking an invalid promise_base is undefined!
///
/// \since 4.0.0
void set_canceled() noexcept {
assert(data_);
std::move(data_)(exception_arg_t{}, exception_t{});
data_ = nullptr;
}
/// Returns true if the continuation is valid (non empty). /// Returns true if the continuation is valid (non empty).
/// ///
/// \throws This method never throws an exception. /// \throws This method never throws an exception.

View File

@ -46,16 +46,25 @@ namespace cti {
/// - *no result*: If the operation didn't finish /// - *no result*: If the operation didn't finish
/// - *a value*: If the operation finished successfully /// - *a value*: If the operation finished successfully
/// - *an exception*: If the operation finished with an exception /// - *an exception*: If the operation finished with an exception
/// or was cancelled.
/// \{ /// \{
/// A class which is convertible to any \ref result and that definitly holds no /// A class which is convertible to any \ref result and that definitely holds no
/// value so the real result gets invalidated when this object is passed to it. /// value so the real result gets invalidated when this object is passed to it.
/// ///
/// \since 4.0.0 /// \since 4.0.0
/// ///
struct empty_result {}; struct empty_result {};
/// A class which is convertible to any result and that definitly holds /// A class which is convertible to any \ref result and that definitely holds
/// a default constructed exception which signals the cancellation of the
/// asynchronous control flow.
///
/// \since 4.0.0
///
struct cancellation_result {};
/// A class which is convertible to any result and that holds
/// an exception which is then passed to the converted result object. /// an exception which is then passed to the converted result object.
/// ///
/// \since 4.0.0 /// \since 4.0.0
@ -73,8 +82,7 @@ public:
explicit exceptional_result(exception_t exception) explicit exceptional_result(exception_t exception)
// NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg) // NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg)
: exception_(std::move(exception)) { : exception_(std::move(exception)) {}
}
exceptional_result& operator=(exception_t exception) { exceptional_result& operator=(exception_t exception) {
// NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg) // NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg)
@ -110,6 +118,7 @@ public:
/// - *no result*: If the operation didn't finish /// - *no result*: If the operation didn't finish
/// - *a value*: If the operation finished successfully /// - *a value*: If the operation finished successfully
/// - *an exception*: If the operation finished with an exception /// - *an exception*: If the operation finished with an exception
/// or was cancelled.
/// ///
/// The interface of the result object is similar to the one proposed in /// The interface of the result object is similar to the one proposed in
/// the `std::expected` proposal: /// the `std::expected` proposal:
@ -132,11 +141,9 @@ class result {
template <typename... Args> template <typename... Args>
explicit result(detail::init_arg_t, Args&&... values) explicit result(detail::init_arg_t, Args&&... values)
: variant_(trait_t::wrap(std::forward<Args>(values)...)) { : variant_(trait_t::wrap(std::forward<Args>(values)...)) {}
}
explicit result(detail::init_arg_t, exception_t exception) explicit result(detail::init_arg_t, exception_t exception)
: variant_(std::move(exception)) { : variant_(std::move(exception)) {}
}
public: public:
using value_t = typename trait_t::value_t; using value_t = typename trait_t::value_t;
@ -144,8 +151,7 @@ public:
template <typename FirstArg, typename... Args> template <typename FirstArg, typename... Args>
explicit result(FirstArg&& first, Args&&... values) explicit result(FirstArg&& first, Args&&... values)
: variant_(trait_t::wrap(std::forward<FirstArg>(first), : variant_(trait_t::wrap(std::forward<FirstArg>(first),
std::forward<Args>(values)...)) { std::forward<Args>(values)...)) {}
}
result() = default; result() = default;
result(result const&) = default; result(result const&) = default;
@ -154,13 +160,13 @@ public:
result& operator=(result&&) = default; result& operator=(result&&) = default;
~result() = default; ~result() = default;
explicit result(exception_t exception) : variant_(std::move(exception)) { explicit result(exception_t exception)
} : variant_(std::move(exception)) {}
result(empty_result) { /* implicit */ result(empty_result) {}
} /* implicit */ result(exceptional_result exceptional_result)
result(exceptional_result exceptional_result) : variant_(std::move(exceptional_result.get_exception())) {}
: variant_(std::move(exceptional_result.get_exception())) { /* implicit */ result(cancellation_result)
} : variant_(exception_t{}) {}
result& operator=(empty_result) { result& operator=(empty_result) {
set_empty(); set_empty();
@ -183,6 +189,10 @@ public:
void set_exception(exception_t exception) { void set_exception(exception_t exception) {
variant_ = std::move(exception); variant_ = std::move(exception);
} }
/// Set the result into a state which holds the cancellation token
void set_canceled() {
variant_ = exception_t{};
}
/// Returns true if the state of the result is empty /// Returns true if the state of the result is empty
bool is_empty() const noexcept { bool is_empty() const noexcept {
@ -192,7 +202,7 @@ public:
bool is_value() const noexcept { bool is_value() const noexcept {
return variant_.template is<surrogate_t>(); return variant_.template is<surrogate_t>();
} }
/// Returns true if the state of the result holds an exception /// Returns true if the state of the result holds a present exception
bool is_exception() const noexcept { bool is_exception() const noexcept {
return variant_.template is<exception_t>(); return variant_.template is<exception_t>();
} }

View File

@ -381,7 +381,7 @@ inline auto invoker_of(identity<void>) {
identity<>{}); identity<>{});
} }
/// - empty_result -> <cancel> /// - empty_result -> <abort>
inline auto invoker_of(identity<empty_result>) { inline auto invoker_of(identity<empty_result>) {
return make_invoker( return make_invoker(
[](auto&& callback, auto&& next_callback, auto&&... args) { [](auto&& callback, auto&& next_callback, auto&&... args) {
@ -392,7 +392,27 @@ inline auto invoker_of(identity<empty_result>) {
std::forward<decltype(args)>(args)...); std::forward<decltype(args)>(args)...);
// Don't invoke anything here since returning an empty result // Don't invoke anything here since returning an empty result
// cancels the asynchronous chain effectively. // aborts the asynchronous chain effectively.
(void)result;
CONTINUABLE_BLOCK_TRY_END
},
identity<>{});
}
/// - cancellation_result -> <cancel>
inline auto invoker_of(identity<cancellation_result>) {
return make_invoker(
[](auto&& callback, auto&& next_callback, auto&&... args) {
(void)next_callback;
CONTINUABLE_BLOCK_TRY_BEGIN
cancellation_result result = invoke_callback(
std::forward<decltype(callback)>(callback),
std::forward<decltype(args)>(args)...);
// Forward the cancellation to the next available exception handler
invoke_no_except(std::forward<decltype(next_callback)>(next_callback),
exception_arg_t{}, exception_t{});
(void)result; (void)result;
CONTINUABLE_BLOCK_TRY_END CONTINUABLE_BLOCK_TRY_END
}, },
@ -409,7 +429,7 @@ inline auto invoker_of(identity<exceptional_result>) {
invoke_callback(std::forward<decltype(callback)>(callback), invoke_callback(std::forward<decltype(callback)>(callback),
std::forward<decltype(args)>(args)...); std::forward<decltype(args)>(args)...);
// Forward the exception to the next available handler // Forward the exception to the next available exception handler
invoke_no_except(std::forward<decltype(next_callback)>(next_callback), invoke_no_except(std::forward<decltype(next_callback)>(next_callback),
exception_arg_t{}, exception_arg_t{},
std::move(result).get_exception()); std::move(result).get_exception());
@ -441,9 +461,13 @@ auto invoker_of(identity<result<Args...>>) {
} else if (result.is_exception()) { } else if (result.is_exception()) {
// Forward the exception to the next available handler // Forward the exception to the next available handler
invoke_no_except( invoke_no_except(std::forward<decltype(next_callback)>(
std::forward<decltype(next_callback)>(next_callback), next_callback),
exception_arg_t{}, std::move(result).get_exception()); exception_arg_t{},
std::move(result).get_exception());
} else {
// Aborts the continuation of the chain
assert(result.is_empty());
} }
// Otherwise the result is empty and we are cancelling our // Otherwise the result is empty and we are cancelling our
@ -543,6 +567,10 @@ public:
std::move(next_callback_)(exception_arg_t{}, std::move(exception)); std::move(next_callback_)(exception_arg_t{}, std::move(exception));
} }
void set_canceled() noexcept {
std::move(next_callback_)(exception_arg_t{}, exception_t{});
}
explicit operator bool() const noexcept { explicit operator bool() const noexcept {
return true; return true;
} }
@ -699,9 +727,13 @@ struct callback_base<identity<Args...>, HandleResults, HandleErrors, Callback,
std::move (*this)(std::move(args)...); std::move (*this)(std::move(args)...);
} }
/// Resolves the continuation with the given error variable. /// Resolves the continuation with the given exception.
void set_exception(exception_t error) noexcept { void set_exception(exception_t exception) noexcept {
std::move (*this)(exception_arg_t{}, std::move(error)); std::move (*this)(exception_arg_t{}, std::move(exception));
}
void set_canceled() noexcept {
std::move (*this)(exception_arg_t{}, exception_t{});
} }
/// Returns true because this is a present continuation /// Returns true because this is a present continuation
@ -768,6 +800,10 @@ struct final_callback : util::non_copyable {
std::move (*this)(exception_arg_t{}, std::move(exception)); std::move (*this)(exception_arg_t{}, std::move(exception));
} }
void set_canceled() noexcept {
std::move (*this)(exception_arg_t{}, exception_t{});
}
explicit operator bool() const noexcept { explicit operator bool() const noexcept {
return true; return true;
} }
@ -872,6 +908,8 @@ struct chained_continuation<identity<Args...>, identity<NextArgs...>,
} else if (result.is_exception()) { } else if (result.is_exception()) {
util::invoke(std::move(proxy), exception_arg_t{}, util::invoke(std::move(proxy), exception_arg_t{},
std::move(result.get_exception())); std::move(result.get_exception()));
} else {
assert(result.is_empty());
} }
} else { } else {
// Invoke the continuation with a proxy callback. // Invoke the continuation with a proxy callback.
@ -927,6 +965,8 @@ struct chained_continuation<identity<Args...>, identity<NextArgs...>,
} else if (result.is_exception()) { } else if (result.is_exception()) {
util::invoke(std::move(proxy), exception_arg_t{}, util::invoke(std::move(proxy), exception_arg_t{},
std::move(result.get_exception())); std::move(result.get_exception()));
} else {
assert(result.is_empty());
} }
} }

View File

@ -93,6 +93,10 @@ public:
std::move (*this)(exception_arg_t{}, std::move(error)); std::move (*this)(exception_arg_t{}, std::move(error));
} }
void set_canceled() noexcept {
std::move (*this)(exception_arg_t{}, exception_t{});
}
explicit operator bool() const noexcept { explicit operator bool() const noexcept {
bool is_valid = operator_bool_or<First, true>::get(first_); bool is_valid = operator_bool_or<First, true>::get(first_);
traverse_pack( traverse_pack(

View File

@ -111,7 +111,7 @@ void assert_async_never_completed(C&& continuable) {
FAIL(); FAIL();
}) })
.fail([](cti::exception_t error) { .fail([](cti::exception_t) {
// ... // ...
FAIL(); FAIL();
}); });

View File

@ -104,6 +104,12 @@ TYPED_TEST(single_dimension_tests, are_not_finished_when_not_continued) {
auto chain = create_incomplete(this); auto chain = create_incomplete(this);
ASSERT_ASYNC_INCOMPLETION(std::move(chain).then(this->supply())); ASSERT_ASYNC_INCOMPLETION(std::move(chain).then(this->supply()));
} }
{
ASSERT_ASYNC_INCOMPLETION(this->supply().then([] {
return empty_result();
}));
}
} }
TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling) { TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling) {
@ -118,6 +124,46 @@ TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling) {
} }
} }
TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling_hook) {
{
ASSERT_ASYNC_CANCELLATION(
this->make(identity<>{}, identity<void>{}, [](auto&& callback) mutable {
std::forward<decltype(callback)>(callback).set_canceled();
}));
}
{
ASSERT_ASYNC_CANCELLATION(
this->make(identity<>{}, identity<void>{}, [](auto&& callback) mutable {
std::forward<decltype(callback)>(callback).set_exception({});
}));
}
{
ASSERT_ASYNC_CANCELLATION(this->supply().then([] {
return exceptional_result(exception_t{});
}));
}
{
ASSERT_ASYNC_CANCELLATION(this->supply().then([]() -> result<> {
return exceptional_result(exception_t{});
}));
}
{
ASSERT_ASYNC_CANCELLATION(this->supply().then([] {
return cancellation_result();
}));
}
{
ASSERT_ASYNC_CANCELLATION(this->supply().then([]() -> result<> {
return cancellation_result();
}));
}
}
TYPED_TEST(single_dimension_tests, freeze_is_kept_across_the_chain) { TYPED_TEST(single_dimension_tests, freeze_is_kept_across_the_chain) {
{ {
auto chain = this->supply().freeze().then([=] { auto chain = this->supply().freeze().then([=] {

View File

@ -56,7 +56,7 @@ TEST(promisify_tests, promisify_with) {
auto c = cti::promisify<int>::with( auto c = cti::promisify<int>::with(
[](auto&& promise, auto&& /*e*/, int const& value) { [](auto&& promise, auto&& /*e*/, int const& value) {
EXPECT_EQ(value, 36354); EXPECT_EQ(value, 36354);
promise.set_exception(cti::exception_t{}); promise.set_exception(supply_test_exception());
}, },
[&](auto&&... args) { [&](auto&&... args) {
async_supply(std::forward<decltype(args)>(args)...); async_supply(std::forward<decltype(args)>(args)...);