Improve the use_continuable_t asio completion tokens such that special mappings are aptable

* Also allow to ignore certain error types through use_continuable_ignoring
* Ref #32
This commit is contained in:
Denis Blank 2020-04-09 17:23:01 +02:00
parent d80f5ef3ec
commit 8187c16ede
3 changed files with 239 additions and 65 deletions

View File

@ -30,62 +30,63 @@
#ifndef CONTINUABLE_DETAIL_ASIO_HPP_INCLUDED #ifndef CONTINUABLE_DETAIL_ASIO_HPP_INCLUDED
#define CONTINUABLE_DETAIL_ASIO_HPP_INCLUDED #define CONTINUABLE_DETAIL_ASIO_HPP_INCLUDED
#include <array>
#include <utility> #include <utility>
#include <continuable/continuable-base.hpp> #include <continuable/continuable-base.hpp>
#include <continuable/detail/core/base.hpp> #include <continuable/detail/core/base.hpp>
#include <continuable/detail/features.hpp> #include <continuable/detail/features.hpp>
#if defined(ASIO_STANDALONE) #if defined(ASIO_STANDALONE)
#include <asio/async_result.hpp> # include <asio/async_result.hpp>
#include <asio/error.hpp> # include <asio/error.hpp>
#include <asio/error_code.hpp> # include <asio/error_code.hpp>
#include <asio/version.hpp> # include <asio/version.hpp>
#if defined(CONTINUABLE_HAS_EXCEPTIONS) # if defined(CONTINUABLE_HAS_EXCEPTIONS)
#include <asio/system_error.hpp> # include <asio/system_error.hpp>
#endif # endif
#if (ASIO_VERSION < 101300) // 1.13.0 # if (ASIO_VERSION < 101300) // 1.13.0
#define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION # define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION
#elif (ASIO_VERSION < 101600) // 1.16.0 (boost 1.72 baseline) # elif (ASIO_VERSION < 101600) // 1.16.0 (boost 1.72 baseline)
#define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION # define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION
#endif # endif
#define CTI_DETAIL_ASIO_NAMESPACE_BEGIN namespace asio { # define CTI_DETAIL_ASIO_NAMESPACE_BEGIN namespace asio {
#define CTI_DETAIL_ASIO_NAMESPACE_END } # define CTI_DETAIL_ASIO_NAMESPACE_END }
#else #else
#include <boost/asio/async_result.hpp> # include <boost/asio/async_result.hpp>
#include <boost/asio/error.hpp> # include <boost/asio/error.hpp>
#include <boost/system/error_code.hpp> # include <boost/system/error_code.hpp>
#include <boost/version.hpp> # include <boost/version.hpp>
#if defined(CONTINUABLE_HAS_EXCEPTIONS) # if defined(CONTINUABLE_HAS_EXCEPTIONS)
#include <boost/system/system_error.hpp> # include <boost/system/system_error.hpp>
#endif # endif
#if (BOOST_VERSION < 107000) // 1.70 # if (BOOST_VERSION < 107000) // 1.70
#define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION # define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION
#elif (BOOST_VERSION < 107200) // 1.72 # elif (BOOST_VERSION < 107200) // 1.72
#define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION # define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION
#endif # endif
#define CTI_DETAIL_ASIO_NAMESPACE_BEGIN \ # define CTI_DETAIL_ASIO_NAMESPACE_BEGIN \
namespace boost { \ namespace boost { \
namespace asio { namespace asio {
#define CTI_DETAIL_ASIO_NAMESPACE_END \ # define CTI_DETAIL_ASIO_NAMESPACE_END \
} \ } \
} }
#endif #endif
#if defined(CTI_DETAIL_ASIO_HAS_NO_INTEGRATION) #if defined(CTI_DETAIL_ASIO_HAS_NO_INTEGRATION)
#error "First-class ASIO support for continuable requires the form of "\ # error "First-class ASIO support for continuable requires the form of "\
"`async_result` with an `initiate` static member function, which was added " \ "`async_result` with an `initiate` static member function, which was added " \
"in standalone ASIO 1.13.0 and Boost ASIO 1.70. Older versions can be " \ "in standalone ASIO 1.13.0 and Boost ASIO 1.70. Older versions can be " \
"integrated manually with `cti::promisify`." "integrated manually with `cti::promisify`."
#endif #endif
#if defined(CONTINUABLE_HAS_EXCEPTIONS) #if defined(CONTINUABLE_HAS_EXCEPTIONS)
#include <exception> # include <exception>
#endif #endif
namespace cti { namespace cti {
@ -96,40 +97,42 @@ namespace asio {
using error_code_t = ::asio::error_code; using error_code_t = ::asio::error_code;
using basic_errors_t = ::asio::error::basic_errors; using basic_errors_t = ::asio::error::basic_errors;
#if defined(CONTINUABLE_HAS_EXCEPTIONS) # if defined(CONTINUABLE_HAS_EXCEPTIONS)
using system_error_t = ::asio::system_error; using system_error_t = ::asio::system_error;
#endif # endif
#else #else
using error_code_t = ::boost::system::error_code; using error_code_t = ::boost::system::error_code;
using basic_errors_t = ::boost::asio::error::basic_errors; using basic_errors_t = ::boost::asio::error::basic_errors;
#if defined(CONTINUABLE_HAS_EXCEPTIONS) # if defined(CONTINUABLE_HAS_EXCEPTIONS)
using system_error_t = ::boost::system::system_error; using system_error_t = ::boost::system::system_error;
#endif # endif
#endif #endif
// Binds `promise` to the first argument of a continuable resolver, giving it // Binds `promise` to the first argument of a continuable resolver, giving it
// the signature of an ASIO handler. // the signature of an ASIO handler.
template <typename Promise> template <typename Promise, typename Token>
auto promise_resolver_handler(Promise&& promise) noexcept { auto promise_resolver_handler(Promise&& promise, Token&& token) noexcept {
return [promise = std::forward<Promise>(promise)]( return [promise = std::forward<Promise>(promise),
error_code_t e, auto&&... args) mutable noexcept { token = std::forward<Token>(token)](error_code_t e,
auto&&... args) mutable noexcept {
if (e) { if (e) {
if (e != basic_errors_t::operation_aborted) { if (!token.is_ignored(e)) {
if (token.is_cancellation(e)) {
promise.set_canceled();
return;
} else {
#if defined(CONTINUABLE_HAS_EXCEPTIONS) #if defined(CONTINUABLE_HAS_EXCEPTIONS)
promise.set_exception( promise.set_exception(
std::make_exception_ptr(system_error_t(std::move(e)))); std::make_exception_ptr(system_error_t(std::move(e))));
#else #else
promise.set_exception(exception_t(e.value(), e.category())); promise.set_exception(exception_t(e.value(), e.category()));
#endif #endif
} else { return;
// Continuable uses a default constructed exception type to signal }
// cancellation to the followed asynchronous control flow. }
promise.set_exception(exception_t{});
} }
} else {
promise.set_value(std::forward<decltype(args)>(args)...); promise.set_value(std::forward<decltype(args)>(args)...);
}
}; };
} }
@ -155,6 +158,51 @@ template <typename... Args>
struct initiate_make_continuable<void(error_code_t const&, Args...)> struct initiate_make_continuable<void(error_code_t const&, Args...)>
: initiate_make_continuable<void(error_code_t, Args...)> {}; : initiate_make_continuable<void(error_code_t, Args...)> {};
struct map_default {
constexpr map_default() noexcept {}
bool is_cancellation(error_code_t const& ec) const noexcept {
// Continuable uses a default constructed exception type to signal
// cancellation to the followed asynchronous control flow.
return ec == basic_errors_t::operation_aborted;
}
bool is_ignored(error_code_t const& /*ec*/) const noexcept {
return false;
}
};
struct map_none {
constexpr map_none() noexcept {}
bool is_cancellation(error_code_t const& /*ec*/) const noexcept {
return false;
}
bool is_ignored(error_code_t const& /*ec*/) const noexcept {
return false;
}
};
template <std::size_t Size>
class map_ignore {
public:
map_ignore(std::array<basic_errors_t, Size> ignored) noexcept
: ignored_(ignored) {}
bool is_cancellation(error_code_t const& ec) const noexcept {
return ec == basic_errors_t::operation_aborted;
}
bool is_ignored(error_code_t const& ec) const noexcept {
for (basic_errors_t ignored : ignored_) {
if (ec == ignored) {
return true;
}
}
return false;
}
private:
std::array<basic_errors_t, Size> ignored_;
};
} // namespace asio } // namespace asio
} // namespace detail } // namespace detail
} // namespace cti } // namespace cti

View File

@ -35,6 +35,21 @@
#include <continuable/detail/utility/traits.hpp> #include <continuable/detail/utility/traits.hpp>
namespace cti { namespace cti {
/// The error code type used by your asio distribution
///
/// \since 4.1.0
using asio_error_code_t = detail::asio::error_code_t;
/// The basic error code enum used by your asio distribution
///
/// \since 4.1.0
using asio_basic_errors_t = detail::asio::basic_errors_t;
/// The system error type used by your asio distribution
///
/// \since 4.1.0
using asio_system_error_t = detail::asio::system_error_t;
/// Type used as an ASIO completion token to specify an asynchronous operation /// Type used as an ASIO completion token to specify an asynchronous operation
/// that should return a continuable_base. /// that should return a continuable_base.
/// ///
@ -60,19 +75,77 @@ namespace cti {
/// }); /// });
/// ``` /// ```
/// ///
/// \tparam Mapper The token can be instantiated with a custom mapper
/// for asio error codes which makes it possible to ignore
/// errors or treat them as cancellation types.
/// The mapper has the following form:
/// ```
/// struct my_mapper {
/// constexpr my_mapper() noexcept {}
///
/// /// Returns true when the error_code_t is a type which represents
/// /// cancellation and
/// bool is_cancellation(error_code_t const& /*ec*/) const noexcept {
/// return false;
/// }
/// bool is_ignored(error_code_t const& /*ec*/) const noexcept {
/// return false;
/// }
/// };
/// ```
///
/// \attention `asio::error::basic_errors::operation_aborted` errors returned
/// by asio are automatically transformed into a default constructed
/// exception type which represents "operation canceled" by the
/// user or program. If you intend to retrieve the full
/// asio::error_code without remapping use the use_continuable_raw_t
/// completion token instead!
///
/// \since 4.0.0 /// \since 4.0.0
struct use_continuable_t {}; template <typename Mapper = detail::asio::map_default>
struct use_continuable_t : public Mapper {
using Mapper::Mapper;
};
/// Special value for instance of `asio_token_t` /// \copydoc use_continuable_t
///
/// The raw async completion handler token does not remap the asio error
/// `asio::error::basic_errors::operation_aborted` to a default constructed
/// exception type.
///
/// \since 4.1.0
using use_continuable_raw_t = use_continuable_t<detail::asio::map_none>;
/// Special value for instance of use_continuable_t which performs remapping
/// of asio error codes to align the cancellation behaviour with the library.
/// ///
/// \copydetails use_continuable_t /// \copydetails use_continuable_t
constexpr use_continuable_t use_continuable{}; constexpr use_continuable_t<> use_continuable{};
/// Special value for instance of use_continuable_raw_t which doesn't perform
/// remapping of asio error codes and rethrows the raw error code.
///
/// \copydetails use_continuable_raw_t
constexpr use_continuable_raw_t use_continuable_raw{};
/// Represents a special asio completion token which treats the given
/// asio basic error codes as success instead of failure.
///
/// `asio::error::basic_errors::operation_aborted` is mapped
/// as cancellation token.
///
/// \since 4.1.0
template <typename... Args>
auto use_continuable_ignoring(Args&&... args) noexcept {
return use_continuable_t<detail::asio::map_ignore<sizeof...(Args)>>{
{asio_basic_errors_t(std::forward<Args>(args))...}};
}
} // namespace cti } // namespace cti
CTI_DETAIL_ASIO_NAMESPACE_BEGIN CTI_DETAIL_ASIO_NAMESPACE_BEGIN
template <typename Signature> template <typename Signature, typename Matcher>
class async_result<cti::use_continuable_t, Signature> { class async_result<cti::use_continuable_t<Matcher>, Signature> {
public: public:
#if defined(CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION) #if defined(CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION)
using return_type = typename cti::detail::asio::initiate_make_continuable< using return_type = typename cti::detail::asio::initiate_make_continuable<
@ -80,16 +153,16 @@ public:
#endif #endif
template <typename Initiation, typename... Args> template <typename Initiation, typename... Args>
static auto initiate(Initiation initiation, cti::use_continuable_t, static auto initiate(Initiation initiation,
Args... args) { cti::use_continuable_t<Matcher> token, Args... args) {
return cti::detail::asio::initiate_make_continuable<Signature>{}( return cti::detail::asio::initiate_make_continuable<Signature>{}(
[initiation = std::move(initiation), [initiation = std::move(initiation), token = std::move(token),
init_args = std::make_tuple(std::move(args)...)]( init_args = std::make_tuple(std::move(args)...)](
auto&& promise) mutable { auto&& promise) mutable {
cti::detail::traits::unpack( cti::detail::traits::unpack(
[initiation = std::move(initiation), [initiation = std::move(initiation),
handler = cti::detail::asio::promise_resolver_handler( handler = cti::detail::asio::promise_resolver_handler(
std::forward<decltype(promise)>(promise))]( std::forward<decltype(promise)>(promise), std::move(token))](
auto&&... args) mutable { auto&&... args) mutable {
std::move(initiation)(std::move(handler), std::move(initiation)(std::move(handler),
std::forward<decltype(args)>(args)...); std::forward<decltype(args)>(args)...);

View File

@ -44,9 +44,7 @@ public:
}) {} }) {}
~async_test_helper() { ~async_test_helper() {
assert(work_); cancel();
timer_.cancel();
work_.reset();
thread_.join(); thread_.join();
} }
@ -55,6 +53,11 @@ public:
return timer_.async_wait(use_continuable); return timer_.async_wait(use_continuable);
} }
void cancel() {
timer_.cancel();
work_.reset();
}
private: private:
asio::io_context context_; asio::io_context context_;
asio::steady_timer timer_; asio::steady_timer timer_;
@ -141,3 +144,53 @@ TYPED_TEST(single_dimension_tests, wait_for_test_async) {
result<> res = helper.wait_for(500ms).apply(cti::transforms::wait_for(50ms)); result<> res = helper.wait_for(500ms).apply(cti::transforms::wait_for(50ms));
ASSERT_FALSE(res.is_exception()); ASSERT_FALSE(res.is_exception());
} }
TYPED_TEST(single_dimension_tests, token_remap_canceled) {
asio::io_context io(1);
asio::steady_timer timer(io, 50ms);
result<> value;
timer.async_wait(use_continuable).next([&](auto&&... args) {
value = result<>::from(std::forward<decltype(args)>(args)...);
});
timer.cancel();
io.run();
ASSERT_TRUE(value.is_exception());
ASSERT_FALSE(bool(value.get_exception()));
}
TYPED_TEST(single_dimension_tests, token_remap_none_raw) {
asio::io_context io(1);
asio::steady_timer timer(io, 50ms);
result<> value;
timer.async_wait(use_continuable_raw).next([&](auto&&... args) {
value = result<>::from(std::forward<decltype(args)>(args)...);
});
timer.cancel();
io.run();
ASSERT_TRUE(value.is_exception());
ASSERT_TRUE(bool(value.get_exception()));
}
TYPED_TEST(single_dimension_tests, token_remap_ignore) {
asio::io_context io(1);
asio::steady_timer timer(io, 50ms);
result<> value;
timer
.async_wait(
use_continuable_ignoring(asio_basic_errors_t::operation_aborted))
.next([&](auto&&... args) {
value = result<>::from(std::forward<decltype(args)>(args)...);
});
timer.cancel();
io.run();
ASSERT_TRUE(value.is_value());
}