From 3ebff60b1a2ea206fd07fc0935072068ddd1ece9 Mon Sep 17 00:00:00 2001 From: Klemens Date: Wed, 31 Aug 2022 10:03:35 +0800 Subject: [PATCH] beast support per-op cancellation - websocket supports cancellation. - Iterating test for ws cancellation. - Only terminal cancellation is forwarded by default. - basic_stream supports cancellation. - supported cancellation is documented. - http cancellation additions. - Added cancellation_slot tests to http, utils and saved_handler. - Added post to write.cpp, to avoid SIGSEV in test. - Refresher describes cancellation in more detail. --- CHANGELOG.md | 7 +- doc/qbk/03_core/1_refresher.qbk | 36 +++ .../beast/_experimental/test/impl/stream.hpp | 10 + include/boost/beast/core/async_base.hpp | 35 ++- include/boost/beast/core/basic_stream.hpp | 78 ++++++ .../core/detail/bind_default_executor.hpp | 10 + .../boost/beast/core/detail/bind_handler.hpp | 44 ++++ .../detail/filtering_cancellation_slot.hpp | 65 +++++ .../boost/beast/core/impl/basic_stream.hpp | 4 + .../boost/beast/core/impl/saved_handler.hpp | 66 ++++- .../boost/beast/core/impl/saved_handler.ipp | 2 + include/boost/beast/core/saved_handler.hpp | 11 +- include/boost/beast/http/impl/write.hpp | 12 + include/boost/beast/http/read.hpp | 52 ++++ include/boost/beast/http/write.hpp | 60 +++++ include/boost/beast/websocket/impl/accept.hpp | 3 + include/boost/beast/websocket/impl/close.hpp | 22 +- include/boost/beast/websocket/impl/ping.hpp | 7 +- include/boost/beast/websocket/impl/read.hpp | 14 +- include/boost/beast/websocket/impl/ssl.hpp | 2 + .../boost/beast/websocket/impl/teardown.hpp | 1 + include/boost/beast/websocket/impl/write.hpp | 9 +- include/boost/beast/websocket/ssl.hpp | 12 + include/boost/beast/websocket/stream.hpp | 136 +++++++++++ include/boost/beast/websocket/teardown.hpp | 11 + test/beast/core/CMakeLists.txt | 1 + test/beast/core/Jamfile | 1 + .../core/filtering_cancellation_slot.cpp | 48 ++++ test/beast/core/saved_handler.cpp | 88 ++++++- test/beast/http/read.cpp | 63 ++++- test/beast/http/write.cpp | 73 ++++++ test/beast/websocket/CMakeLists.txt | 1 + test/beast/websocket/Jamfile | 1 + test/beast/websocket/cancel.cpp | 230 ++++++++++++++++++ test/doc/core_1_refresher.cpp | 30 +++ 35 files changed, 1216 insertions(+), 29 deletions(-) create mode 100644 include/boost/beast/core/detail/filtering_cancellation_slot.hpp create mode 100644 test/beast/core/filtering_cancellation_slot.cpp create mode 100644 test/beast/websocket/cancel.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8ec12b..fb7f37f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Version 342: + +* Support per operation cancellation + +-------------------------------------------------------------------------------- + Version 341: * Expect header field with the "100-continue" is handled in upgrade. @@ -19,7 +25,6 @@ Version 339: -------------------------------------------------------------------------------- - Version 338: * Added per message compression options. diff --git a/doc/qbk/03_core/1_refresher.qbk b/doc/qbk/03_core/1_refresher.qbk index 2f6dd9c7..5cd89649 100644 --- a/doc/qbk/03_core/1_refresher.qbk +++ b/doc/qbk/03_core/1_refresher.qbk @@ -199,6 +199,10 @@ has both an [@boost:/doc/html/boost_asio/overview/core/allocation.html ['associated allocator]] returned by [@boost:/doc/html/boost_asio/reference/get_associated_allocator.html `net::get_associated_allocator`], +, an +[@boost:/doc/html/boost_asio/reference/associated_cancellation_slot.html ['associated cancellation slot]] +returned by +[@boost:/doc/html/boost_asio/reference/associated_cancellation_slot.html `net::get_associated_cancellation_slot`]. and an [@boost:/doc/html/boost_asio/reference/associated_executor.html ['associated executor]] returned by @@ -210,6 +214,8 @@ These associations may be specified intrusively: Or these associations may be specified non-intrusively, by specializing the class templates [@boost:/doc/html/boost_asio/reference/associated_allocator.html `net::associated_allocator`] +, +[@boost:/doc/html/boost_asio/reference/associated_cancellation_slot.html `net::associated_cancellation_slot`] and [@boost:/doc/html/boost_asio/reference/associated_executor.html `net::associated_executor`]: @@ -227,6 +233,36 @@ object providing the algorithm used to invoke the completion handler. Unless customized by the caller, a completion handler defaults to using `std::allocator` and the executor of the corresponding I/O object. +The function +[@boost:/doc/html/boost_asio/reference/bind_allocator.html `net::bind_allocator`] +can be used whent he caller wants to assign a custom allocator to the operation. + + +A completion token's associated cancellation_slot can be used to cancel single +operations. This is often passed through by the completion token such as +[@boost:/doc/html/boost_asio/reference/use_awaitable.html `net::use_awaitable`] +or +[@boost:/doc/html/boost_asio/reference/yield_context.html `net::yield_context`] +. + +The available [@boost:/doc/html/boost_asio/reference/cancellation_type.html cancellation types] are listed below. + +# `terminal` + Requests cancellation where, following a successful cancellation, + the only safe operations on the I/O object are closure or destruction. + +# `partial` + Requests cancellation where a successful cancellation may result in partial + side effects or no side effects. Following cancellation, + the I/O object is in a well-known state, and may be used for further operations. + +# `total` + Requests cancellation where a successful cancellation results in no apparent side effects. + Following cancellation, the I/O object is in the same observable state as it was prior to the operation. + + + + Networking prescribes facilities to determine the context in which handlers run. Every I/O object refers to an __ExecutionContext__ for obtaining the __Executor__ instance used to invoke completion handlers. diff --git a/include/boost/beast/_experimental/test/impl/stream.hpp b/include/boost/beast/_experimental/test/impl/stream.hpp index af5ed67a..90dd6252 100644 --- a/include/boost/beast/_experimental/test/impl/stream.hpp +++ b/include/boost/beast/_experimental/test/impl/stream.hpp @@ -77,6 +77,16 @@ class basic_stream::read_op : public detail::stream_read_op_base return net::get_associated_allocator(h_); } + using cancellation_slot_type = + net::associated_cancellation_slot_t; + + cancellation_slot_type + get_cancellation_slot() const noexcept + { + return net::get_associated_cancellation_slot(h_, + net::cancellation_slot()); + } + void operator()(error_code ec) { diff --git a/include/boost/beast/core/async_base.hpp b/include/boost/beast/core/async_base.hpp index 9e9acc17..f8548514 100644 --- a/include/boost/beast/core/async_base.hpp +++ b/include/boost/beast/core/async_base.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -187,7 +188,7 @@ class async_base Handler h_; detail::select_work_guard_t wg1_; - + net::cancellation_type act_{net::cancellation_type::terminal}; public: /** The type of executor associated with this object. @@ -308,6 +309,38 @@ public: h_, wg1_.get_executor()); } + /** The type of cancellation_slot associated with this object. + + If a class derived from @ref async_base is a completion + handler, then the associated cancellation_slot of the + derived class will be this type. + + The default type is a filtering cancellation slot, + that only allows terminal cancellation. + */ + using cancellation_slot_type = + beast::detail::filtering_cancellation_slot>; + + /** Returns the cancellation_slot associated with this object. + + If a class derived from @ref async_base is a completion + handler, then the object returned from this function will be used + as the associated cancellation_slot of the derived class. + */ + cancellation_slot_type + get_cancellation_slot() const noexcept + { + return cancellation_slot_type(act_, net::get_associated_cancellation_slot(h_, + net::cancellation_slot())); + } + + /// Set the allowed cancellation types, default is `terminal`. + void set_allowed_cancellation( + net::cancellation_type allowed_cancellation_types = net::cancellation_type::terminal) + { + act_ = allowed_cancellation_types; + } + /// Returns the handler associated with this object Handler const& handler() const noexcept diff --git a/include/boost/beast/core/basic_stream.hpp b/include/boost/beast/core/basic_stream.hpp index 7364f5a8..59d39de7 100644 --- a/include/boost/beast/core/basic_stream.hpp +++ b/include/boost/beast/core/basic_stream.hpp @@ -919,6 +919,17 @@ public: this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_connect operation. + @see async_connect */ template< @@ -972,6 +983,18 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_connect operation. + */ template< class EndpointSequence, @@ -1064,6 +1087,17 @@ public: } }; @endcode + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_connect operation. */ template< class EndpointSequence, @@ -1129,6 +1163,17 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_connect operation. */ template< class Iterator, @@ -1189,6 +1234,17 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_connect operation. */ template< class Iterator, @@ -1316,6 +1372,17 @@ public: number of bytes. Consider using the function `net::async_read` if you need to ensure that the requested amount of data is read before the asynchronous operation completes. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_read_some operation. */ template< class MutableBufferSequence, @@ -1439,6 +1506,17 @@ public: number of bytes. Consider using the function `net::async_write` if you need to ensure that the requested amount of data is sent before the asynchronous operation completes. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_write_some operation. */ template< class ConstBufferSequence, diff --git a/include/boost/beast/core/detail/bind_default_executor.hpp b/include/boost/beast/core/detail/bind_default_executor.hpp index c34a9027..9dd050a1 100644 --- a/include/boost/beast/core/detail/bind_default_executor.hpp +++ b/include/boost/beast/core/detail/bind_default_executor.hpp @@ -67,6 +67,16 @@ public: h_, this->get()); } + using cancellation_slot_type = + net::associated_cancellation_slot_t; + + cancellation_slot_type + get_cancellation_slot() const noexcept + { + return net::get_associated_cancellation_slot(h_, + net::cancellation_slot()); + } + // The allocation hooks are still defined because they trivially forward to // user hooks. Forward here ensures that the user will get a compile error // if they build their code with BOOST_ASIO_NO_DEPRECATED. diff --git a/include/boost/beast/core/detail/bind_handler.hpp b/include/boost/beast/core/detail/bind_handler.hpp index 07db8d4b..76f69a41 100644 --- a/include/boost/beast/core/detail/bind_handler.hpp +++ b/include/boost/beast/core/detail/bind_handler.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,9 @@ class bind_wrapper template friend struct net::associated_allocator; + template + friend struct net::associated_cancellation_slot; + template static typename std::enable_if< @@ -217,6 +221,10 @@ class bind_front_wrapper template friend struct net::associated_allocator; + template + friend struct net::associated_cancellation_slot; + + template void invoke( @@ -385,6 +393,42 @@ struct associated_allocator< } }; +template +struct associated_cancellation_slot< + beast::detail::bind_wrapper, CancellationSlot> +{ + using type = typename + associated_cancellation_slot::type; + + static + type + get(beast::detail::bind_wrapper const& op, + CancellationSlot const& slot = CancellationSlot{}) noexcept + { + return associated_cancellation_slot< + Handler, CancellationSlot>::get(op.h_, slot); + } +}; + +template +struct associated_cancellation_slot< + beast::detail::bind_front_wrapper, CancellationSlot> +{ + using type = typename + associated_cancellation_slot::type; + + static + type + get(beast::detail::bind_front_wrapper const& op, + CancellationSlot const& slot = CancellationSlot{}) noexcept + { + return associated_cancellation_slot< + Handler, CancellationSlot>::get(op.h_, slot); + } +}; + + + } // asio } // boost diff --git a/include/boost/beast/core/detail/filtering_cancellation_slot.hpp b/include/boost/beast/core/detail/filtering_cancellation_slot.hpp new file mode 100644 index 00000000..dc2b6e38 --- /dev/null +++ b/include/boost/beast/core/detail/filtering_cancellation_slot.hpp @@ -0,0 +1,65 @@ +// +// Copyright (c) 2022 Klemens Morgenstern (klemens.morgenstern@gmx.net) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_BEAST_CORE_DETAIL_FILTERING_CANCELLATION_SLOT_HPP +#define BOOST_BEAST_CORE_DETAIL_FILTERING_CANCELLATION_SLOT_HPP + +#include +#include + +namespace boost { +namespace beast { +namespace detail { + +template +struct filtering_cancellation_slot : CancellationSlot +{ + template + filtering_cancellation_slot(net::cancellation_type type, Args && ... args) + : CancellationSlot(std::forward(args)...), type(type) {} + + net::cancellation_type type = net::cancellation_type::terminal; + + using CancellationSlot::operator=; + + template + struct handler_wrapper + { + Handler handler; + const net::cancellation_type type; + + template + handler_wrapper(net::cancellation_type type, Args && ... args) + : handler(std::forward(args)...), + type(type) {} + + void operator()(net::cancellation_type tp) + { + if ((tp & type) != net::cancellation_type::none) + handler(tp); + } + }; + + template + CancellationHandler& emplace(Args && ... args) + { + return CancellationSlot::template emplace>( + type, std::forward(args)...).handler; + } + + template + CancellationHandler& assign(CancellationHandler && ch) + { + return CancellationSlot::template emplace>( + type, std::forward(ch)).handler; + } +}; + +} +} +} +#endif //BOOST_BEAST_CORE_DETAIL_FILTERING_CANCELLATION_SLOT_HPP diff --git a/include/boost/beast/core/impl/basic_stream.hpp b/include/boost/beast/core/impl/basic_stream.hpp index f0a0645e..1c47c37a 100644 --- a/include/boost/beast/core/impl/basic_stream.hpp +++ b/include/boost/beast/core/impl/basic_stream.hpp @@ -283,6 +283,7 @@ public: , pg_() , b_(b) { + this->set_allowed_cancellation(net::cancellation_type::all); if (buffer_bytes(b_) == 0 && state().pending) { // Workaround: @@ -452,6 +453,7 @@ public: , pg0_(impl_->read.pending) , pg1_(impl_->write.pending) { + this->set_allowed_cancellation(net::cancellation_type::all); if(state().timer.expiry() != stream_base::never()) { BOOST_ASIO_HANDLER_LOCATION(( @@ -489,6 +491,7 @@ public: , pg0_(impl_->read.pending) , pg1_(impl_->write.pending) { + this->set_allowed_cancellation(net::cancellation_type::all); if(state().timer.expiry() != stream_base::never()) { BOOST_ASIO_HANDLER_LOCATION(( @@ -526,6 +529,7 @@ public: , pg0_(impl_->read.pending) , pg1_(impl_->write.pending) { + this->set_allowed_cancellation(net::cancellation_type::all); if(state().timer.expiry() != stream_base::never()) { BOOST_ASIO_HANDLER_LOCATION(( diff --git a/include/boost/beast/core/impl/saved_handler.hpp b/include/boost/beast/core/impl/saved_handler.hpp index a6cd131d..8abddb40 100644 --- a/include/boost/beast/core/impl/saved_handler.hpp +++ b/include/boost/beast/core/impl/saved_handler.hpp @@ -12,7 +12,9 @@ #include #include +#include #include +#include #include #include #include @@ -28,9 +30,10 @@ class saved_handler::base { protected: ~base() = default; - + saved_handler * owner_; public: - base() = default; + base(saved_handler * owner) : owner_(owner){} + void set_owner(saved_handler * new_owner) { owner_ = new_owner;} virtual void destroy() = 0; virtual void invoke() = 0; }; @@ -72,11 +75,12 @@ class saved_handler::impl final : public base net::executor_work_guard< net::associated_executor_t> wg2_; #endif // defined(BOOST_ASIO_NO_TS_EXECUTORS) - + net::cancellation_slot slot_{net::get_associated_cancellation_slot(v_.h)}; public: template - impl(alloc_type const& a, Handler_&& h) - : v_(a, std::forward(h)) + impl(alloc_type const& a, Handler_&& h, + saved_handler * owner) + : base(owner), v_(a, std::forward(h)) #if defined(BOOST_ASIO_NO_TS_EXECUTORS) , wg2_(net::prefer( net::get_associated_executor(v_.h), @@ -87,10 +91,15 @@ public: { } + ~impl() + { + } + void destroy() override { auto v = std::move(v_); + slot_.clear(); alloc_traits::destroy(v.get(), this); alloc_traits::deallocate(v.get(), this, 1); } @@ -98,11 +107,22 @@ public: void invoke() override { + slot_.clear(); auto v = std::move(v_); alloc_traits::destroy(v.get(), this); alloc_traits::deallocate(v.get(), this, 1); v.h(); } + + void self_complete() + { + slot_.clear(); + owner_->p_ = nullptr; + auto v = std::move(v_); + alloc_traits::destroy(v.get(), this); + alloc_traits::deallocate(v.get(), this, 1); + v.h(net::error::operation_aborted); + } }; //------------------------------------------------------------------------------ @@ -110,7 +130,8 @@ public: template void saved_handler:: -emplace(Handler&& handler, Allocator const& alloc) +emplace(Handler&& handler, Allocator const& alloc, + net::cancellation_type cancel_type) { // Can't delete a handler before invoking BOOST_ASSERT(! has_value()); @@ -140,22 +161,47 @@ emplace(Handler&& handler, Allocator const& alloc) alloc_traits::deallocate(a, p, 1); } }; + + + auto cancel_slot = net::get_associated_cancellation_slot(handler); storage s(alloc); alloc_traits::construct(s.a, s.p, - s.a, std::forward(handler)); - p_ = boost::exchange(s.p, nullptr); + s.a, std::forward(handler), this); + + auto tmp = boost::exchange(s.p, nullptr); + p_ = tmp; + + if (cancel_slot.is_connected()) + { + struct cancel_op + { + impl* p; + net::cancellation_type accepted_ct; + cancel_op(impl* p, + net::cancellation_type accepted_ct) + : p(p), accepted_ct(accepted_ct) {} + + void operator()(net::cancellation_type ct) + { + if ((ct & accepted_ct) != net::cancellation_type::none) + p->self_complete(); + } + }; + cancel_slot.template emplace(tmp, cancel_type); + } } template void saved_handler:: -emplace(Handler&& handler) +emplace(Handler&& handler, net::cancellation_type cancel_type) { // Can't delete a handler before invoking BOOST_ASSERT(! has_value()); emplace( std::forward(handler), - net::get_associated_allocator(handler)); + net::get_associated_allocator(handler), + cancel_type); } } // beast diff --git a/include/boost/beast/core/impl/saved_handler.ipp b/include/boost/beast/core/impl/saved_handler.ipp index 30ecbc8e..f9ce0660 100644 --- a/include/boost/beast/core/impl/saved_handler.ipp +++ b/include/boost/beast/core/impl/saved_handler.ipp @@ -27,6 +27,7 @@ saved_handler:: saved_handler(saved_handler&& other) noexcept : p_(boost::exchange(other.p_, nullptr)) { + p_->set_owner(this); } saved_handler& @@ -36,6 +37,7 @@ operator=(saved_handler&& other) noexcept // Can't delete a handler before invoking BOOST_ASSERT(! has_value()); p_ = boost::exchange(other.p_, nullptr); + p_->set_owner(this); return *this; } diff --git a/include/boost/beast/core/saved_handler.hpp b/include/boost/beast/core/saved_handler.hpp index a4599d42..985f65d4 100644 --- a/include/boost/beast/core/saved_handler.hpp +++ b/include/boost/beast/core/saved_handler.hpp @@ -11,6 +11,7 @@ #define BOOST_BEAST_CORE_SAVED_HANDLER_HPP #include +#include namespace boost { namespace beast { @@ -72,10 +73,13 @@ public: The implementation takes ownership of the handler by performing a decay-copy. @param alloc The allocator to use. + + @param cancel_type The type of cancellation allowed to complete this op. */ template void - emplace(Handler&& handler, Allocator const& alloc); + emplace(Handler&& handler, Allocator const& alloc, + net::cancellation_type cancel_type = net::cancellation_type::terminal); /** Store a completion handler in the container. @@ -85,10 +89,13 @@ public: @param handler The completion handler to store. The implementation takes ownership of the handler by performing a decay-copy. + + @param cancel_type The type of cancellation allowed to complete this op. */ template void - emplace(Handler&& handler); + emplace(Handler&& handler, + net::cancellation_type cancel_type = net::cancellation_type::terminal); /** Discard the saved handler, if one exists. diff --git a/include/boost/beast/http/impl/write.hpp b/include/boost/beast/http/impl/write.hpp index c0819a16..51f6c6a1 100644 --- a/include/boost/beast/http/impl/write.hpp +++ b/include/boost/beast/http/impl/write.hpp @@ -178,8 +178,18 @@ class write_op Stream& s_; serializer& sr_; std::size_t bytes_transferred_ = 0; + net::cancellation_state st_{this-> + beast::async_base> + ::get_cancellation_slot()}; public: + using cancellation_slot_type = net::cancellation_slot; + cancellation_slot_type get_cancellation_slot() const noexcept + { + return st_.slot(); + } + + template write_op( Handler_&& h, @@ -227,6 +237,8 @@ public: s_, sr_, std::move(*this)); } bytes_transferred_ += bytes_transferred; + if (!ec && st_.cancelled() != net::cancellation_type::none) + ec = net::error::operation_aborted; if(ec) goto upcall; if(Predicate{}(sr_)) diff --git a/include/boost/beast/http/read.hpp b/include/boost/beast/http/read.hpp index 492f2cfc..1d0d8e9f 100644 --- a/include/boost/beast/http/read.hpp +++ b/include/boost/beast/http/read.hpp @@ -200,6 +200,19 @@ read_some( @note The completion handler will receive as a parameter the total number of bytes transferred from the stream. This may be zero for the case where there is sufficient pre-existing message data in the dynamic buffer. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + */ template< class AsyncReadStream, @@ -397,6 +410,19 @@ read_header( there is sufficient pre-existing message data in the dynamic buffer. The implementation will call @ref basic_parser::eager with the value `false` on the parser passed in. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + */ template< class AsyncReadStream, @@ -594,6 +620,19 @@ read( there is sufficient pre-existing message data in the dynamic buffer. The implementation will call @ref basic_parser::eager with the value `true` on the parser passed in. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + */ template< class AsyncReadStream, @@ -800,6 +839,19 @@ read( there is sufficient pre-existing message data in the dynamic buffer. The implementation will call @ref basic_parser::eager with the value `true` on the parser passed in. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + */ template< class AsyncReadStream, diff --git a/include/boost/beast/http/write.hpp b/include/boost/beast/http/write.hpp index be582fb0..50022e37 100644 --- a/include/boost/beast/http/write.hpp +++ b/include/boost/beast/http/write.hpp @@ -161,6 +161,18 @@ write_some( this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + @see serializer */ template< @@ -291,6 +303,18 @@ write_header( @note The implementation will call @ref serializer::split with the value `true` on the serializer passed in. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + @see serializer */ template< @@ -412,6 +436,18 @@ write( this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + @see serializer */ template< @@ -637,6 +673,18 @@ write( this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + @see message */ template< @@ -699,6 +747,18 @@ async_write( this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + + if the `stream` also supports terminal cancellation. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. + @see message */ template< diff --git a/include/boost/beast/websocket/impl/accept.hpp b/include/boost/beast/websocket/impl/accept.hpp index c537a20c..aa6b05c8 100644 --- a/include/boost/beast/websocket/impl/accept.hpp +++ b/include/boost/beast/websocket/impl/accept.hpp @@ -266,6 +266,9 @@ public: // read and respond to an upgrade request // +// Cancellation: the async_accept cancellation can be terminal +// because it will just interrupt the reading of the header. +// template template class stream::accept_op diff --git a/include/boost/beast/websocket/impl/close.hpp b/include/boost/beast/websocket/impl/close.hpp index d32d9830..7b0bf539 100644 --- a/include/boost/beast/websocket/impl/close.hpp +++ b/include/boost/beast/websocket/impl/close.hpp @@ -90,9 +90,15 @@ public: BOOST_ASIO_HANDLER_LOCATION(( __FILE__, __LINE__, "websocket::async_close")); - - impl.op_close.emplace(std::move(*this)); + this->set_allowed_cancellation(net::cancellation_type::all); + impl.op_close.emplace(std::move(*this), + net::cancellation_type::all); } + // cancel fired before we could do anything. + if (ec == net::error::operation_aborted) + return this->complete(cont, ec); + this->set_allowed_cancellation(net::cancellation_type::terminal); + impl.wr_block.lock(this); BOOST_ASIO_CORO_YIELD { @@ -143,9 +149,17 @@ public: BOOST_ASIO_HANDLER_LOCATION(( __FILE__, __LINE__, "websocket::async_close")); - + // terminal only, that's the default impl.op_r_close.emplace(std::move(*this)); } + if (ec == net::error::operation_aborted) + { + // if a cancellation fires here, we do a dirty shutdown + impl.change_status(status::closed); + close_socket(get_lowest_layer(impl.stream())); + return this->complete(cont, ec); + } + impl.rd_block.lock(this); BOOST_ASIO_CORO_YIELD { @@ -185,7 +199,7 @@ public: beast::detail::bind_continuation(std::move(*this))); } impl.rd_buf.commit(bytes_transferred); - if(impl.check_stop_now(ec)) + if(impl.check_stop_now(ec)) //< this catches cancellation goto upcall; } if(detail::is_control(impl.rd_fh.op)) diff --git a/include/boost/beast/websocket/impl/ping.hpp b/include/boost/beast/websocket/impl/ping.hpp index dd1233ec..acfee695 100644 --- a/include/boost/beast/websocket/impl/ping.hpp +++ b/include/boost/beast/websocket/impl/ping.hpp @@ -86,9 +86,12 @@ public: BOOST_ASIO_HANDLER_LOCATION(( __FILE__, __LINE__, "websocket::async_ping")); - - impl.op_ping.emplace(std::move(*this)); + this->set_allowed_cancellation(net::cancellation_type::all); + impl.op_ping.emplace(std::move(*this), net::cancellation_type::all); } + if (ec) + return this->complete(cont, ec); + this->set_allowed_cancellation(net::cancellation_type::terminal); impl.wr_block.lock(this); BOOST_ASIO_CORO_YIELD { diff --git a/include/boost/beast/websocket/impl/read.hpp b/include/boost/beast/websocket/impl/read.hpp index f1f9ac87..d8e89249 100644 --- a/include/boost/beast/websocket/impl/read.hpp +++ b/include/boost/beast/websocket/impl/read.hpp @@ -107,8 +107,14 @@ public: __FILE__, __LINE__, "websocket::async_read_some")); - impl.op_r_rd.emplace(std::move(*this)); + this->set_allowed_cancellation(net::cancellation_type::all); + impl.op_r_rd.emplace(std::move(*this), net::cancellation_type::all); } + if (ec) + return this->complete(cont, ec, bytes_written_); + + this->set_allowed_cancellation(net::cancellation_type::terminal); + impl.rd_block.lock(this); BOOST_ASIO_CORO_YIELD { @@ -275,6 +281,9 @@ public: impl.op_rd.emplace(std::move(*this)); } + if (ec) + return this->complete(cont, ec, bytes_written_); + impl.wr_block.lock(this); BOOST_ASIO_CORO_YIELD { @@ -629,6 +638,9 @@ public: impl.op_rd.emplace(std::move(*this)); } + if (ec) + return this->complete(cont, ec, bytes_written_); + impl.wr_block.lock(this); BOOST_ASIO_CORO_YIELD { diff --git a/include/boost/beast/websocket/impl/ssl.hpp b/include/boost/beast/websocket/impl/ssl.hpp index 7b9bdd63..74654089 100644 --- a/include/boost/beast/websocket/impl/ssl.hpp +++ b/include/boost/beast/websocket/impl/ssl.hpp @@ -66,6 +66,8 @@ struct ssl_shutdown_op { BOOST_ASIO_CORO_REENTER(*this) { + self.reset_cancellation_state(net::enable_total_cancellation()); + BOOST_ASIO_CORO_YIELD s_.async_shutdown(std::move(self)); ec_ = ec; diff --git a/include/boost/beast/websocket/impl/teardown.hpp b/include/boost/beast/websocket/impl/teardown.hpp index 3dfc3d2f..7355f015 100644 --- a/include/boost/beast/websocket/impl/teardown.hpp +++ b/include/boost/beast/websocket/impl/teardown.hpp @@ -59,6 +59,7 @@ public: , nb_(false) { (*this)({}, 0, false); + this->set_allowed_cancellation(net::cancellation_type::all); } void diff --git a/include/boost/beast/websocket/impl/write.hpp b/include/boost/beast/websocket/impl/write.hpp index 217a8ae3..04913ef3 100644 --- a/include/boost/beast/websocket/impl/write.hpp +++ b/include/boost/beast/websocket/impl/write.hpp @@ -181,9 +181,14 @@ operator()( "websocket::async_write" : "websocket::async_write_some" )); - - impl.op_wr.emplace(std::move(*this)); + this->set_allowed_cancellation(net::cancellation_type::all); + impl.op_wr.emplace(std::move(*this), + net::cancellation_type::all); } + if (ec) + return this->complete(cont, ec, bytes_transferred_); + + this->set_allowed_cancellation(net::cancellation_type::terminal); impl.wr_block.lock(this); BOOST_ASIO_CORO_YIELD { diff --git a/include/boost/beast/websocket/ssl.hpp b/include/boost/beast/websocket/ssl.hpp index b7d62a37..3fe63f41 100644 --- a/include/boost/beast/websocket/ssl.hpp +++ b/include/boost/beast/websocket/ssl.hpp @@ -68,6 +68,18 @@ teardown( this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_teardown + and @c async_shutdown operation. + */ template void diff --git a/include/boost/beast/websocket/stream.hpp b/include/boost/beast/websocket/stream.hpp index 571c7936..0665dfa9 100644 --- a/include/boost/beast/websocket/stream.hpp +++ b/include/boost/beast/websocket/stream.hpp @@ -1602,6 +1602,20 @@ public: this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + @note `terminal` cancellation will may close the underlying socket. + @see @li Websocket Closing Handshake (RFC6455) */ @@ -1706,6 +1720,21 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< BOOST_BEAST_ASYNC_TPARAM1 WriteHandler = @@ -1814,6 +1843,21 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< BOOST_BEAST_ASYNC_TPARAM1 WriteHandler = @@ -1975,6 +2019,21 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< class DynamicBuffer, @@ -2154,6 +2213,21 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< class DynamicBuffer, @@ -2258,6 +2332,21 @@ public: from the beginning. @param ec Set to indicate what error occurred, if any. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template std::size_t @@ -2329,6 +2418,22 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< class MutableBufferSequence, @@ -2452,6 +2557,21 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< class ConstBufferSequence, @@ -2490,6 +2610,7 @@ public: @return The number of bytes sent from the buffers. @throws system_error Thrown on failure. + */ template std::size_t @@ -2575,6 +2696,21 @@ public: immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::total + + `total` cancellation succeeds if the operation is suspended due to ongoing + control operations such as a ping/pong. + `terminal` cancellation succeeds when supported by the underlying stream. + + `terminal` cancellation leaves the stream in an undefined state, + so that only closing it is guaranteed to succeed. */ template< class ConstBufferSequence, diff --git a/include/boost/beast/websocket/teardown.hpp b/include/boost/beast/websocket/teardown.hpp index 257cb1cb..5ffbdcd8 100644 --- a/include/boost/beast/websocket/teardown.hpp +++ b/include/boost/beast/websocket/teardown.hpp @@ -163,6 +163,17 @@ teardown( this function. Invocation of the handler will be performed in a manner equivalent to using `net::post`. + @par Per-Operation Cancellation + + This asynchronous operation supports cancellation for the following + net::cancellation_type values: + + @li @c net::cancellation_type::terminal + @li @c net::cancellation_type::partial + @li @c net::cancellation_type::total + + if they are also supported by the socket's @c async_wait operation. + */ template< class Protocol, class Executor, diff --git a/test/beast/core/CMakeLists.txt b/test/beast/core/CMakeLists.txt index e631a823..51e56dd2 100644 --- a/test/beast/core/CMakeLists.txt +++ b/test/beast/core/CMakeLists.txt @@ -48,6 +48,7 @@ add_executable (tests-beast-core file_posix.cpp file_stdio.cpp file_win32.cpp + filtering_cancellation_slot.cpp flat_buffer.cpp flat_static_buffer.cpp flat_stream.cpp diff --git a/test/beast/core/Jamfile b/test/beast/core/Jamfile index 80fbe3b0..99b36957 100644 --- a/test/beast/core/Jamfile +++ b/test/beast/core/Jamfile @@ -39,6 +39,7 @@ local SOURCES = file_posix.cpp file_stdio.cpp file_win32.cpp + filtering_cancellation_slot.cpp flat_buffer.cpp flat_static_buffer.cpp flat_stream.cpp diff --git a/test/beast/core/filtering_cancellation_slot.cpp b/test/beast/core/filtering_cancellation_slot.cpp new file mode 100644 index 00000000..f8334cb6 --- /dev/null +++ b/test/beast/core/filtering_cancellation_slot.cpp @@ -0,0 +1,48 @@ +// +// Copyright (c) 2022 Klemens Morgenstern (klemens.morgenstern@gmx.net) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + + +// Test that header file is self-contained. +#include + +#include + +namespace boost { +namespace beast { + + +struct filtering_cancellation_slot_test : beast::unit_test::suite +{ + void + run() + { + using ct = net::cancellation_type; + ct fired = ct::none; + auto l = [&fired](ct tp){fired = tp;}; + + net::cancellation_signal sl; + + detail::filtering_cancellation_slot<> slot{ct::terminal, sl.slot()}; + slot.type |= ct::total; + slot = sl.slot(); + + slot.assign(l); + + BEAST_EXPECT(fired == ct::none); + sl.emit(ct::total); + BEAST_EXPECT(fired == ct::total); + sl.emit(ct::partial); + BEAST_EXPECT(fired == ct::total); + sl.emit(ct::terminal); + BEAST_EXPECT(fired == ct::terminal); + } +}; + +BEAST_DEFINE_TESTSUITE(beast,core,filtering_cancellation_slot); + +} // beast +} // boost \ No newline at end of file diff --git a/test/beast/core/saved_handler.cpp b/test/beast/core/saved_handler.cpp index 57a105c6..f3fba935 100644 --- a/test/beast/core/saved_handler.cpp +++ b/test/beast/core/saved_handler.cpp @@ -9,7 +9,7 @@ // Test that header file is self-contained. #include - +#include #include #include @@ -46,7 +46,7 @@ public: } void - operator()() + operator()(system::error_code ec_ = {}) { failed_ = false; } @@ -74,7 +74,7 @@ public: } void - operator()() + operator()(system::error_code = {}) { invoked_ = true; } @@ -90,7 +90,7 @@ public: } void - operator()() + operator()(system::error_code = {}) { } }; @@ -119,7 +119,7 @@ public: { saved_handler sh; try - { + { sh.emplace(throwing_handler{}); fail(); } @@ -131,10 +131,88 @@ public: } } + void + testSavedHandlerCancellation() + { + { + net::cancellation_signal sig; + + saved_handler sh; + BEAST_EXPECT(! sh.has_value()); + + sh.emplace( + net::bind_cancellation_slot( + sig.slot(), handler{})); + BEAST_EXPECT(sh.has_value()); + BEAST_EXPECT(sig.slot().has_handler()); + sig.emit(net::cancellation_type::all); + BEAST_EXPECT(! sh.has_value()); + BEAST_EXPECT(!sig.slot().has_handler()); + + + sh.emplace( + net::bind_cancellation_slot( + sig.slot(), handler{})); + BEAST_EXPECT(sh.has_value()); + BEAST_EXPECT(sig.slot().has_handler()); + sig.emit(net::cancellation_type::total); + BEAST_EXPECT(sh.has_value()); + BEAST_EXPECT(sig.slot().has_handler()); + sig.emit(net::cancellation_type::terminal); + BEAST_EXPECT(! sh.has_value()); + BEAST_EXPECT(!sig.slot().has_handler()); + + sh.emplace( + net::bind_cancellation_slot( + sig.slot(), handler{}), + net::cancellation_type::total); + BEAST_EXPECT(sh.has_value()); + BEAST_EXPECT(sig.slot().has_handler()); + sig.emit(net::cancellation_type::total); + BEAST_EXPECT(! sh.has_value()); + BEAST_EXPECT(!sig.slot().has_handler()); + + { + saved_handler sh_inner; + sh_inner.emplace( + net::bind_cancellation_slot( + sig.slot(), handler{})); + + sh = std::move(sh_inner); + } + BEAST_EXPECT(sh.has_value()); + BEAST_EXPECT(sig.slot().has_handler()); + sig.emit(net::cancellation_type::all); + BEAST_EXPECT(! sh.has_value()); + BEAST_EXPECT(!sig.slot().has_handler()); + + } + { + saved_handler sh; + net::cancellation_signal sig; + + try + { + sh.emplace( + net::bind_cancellation_slot( + sig.slot(), + throwing_handler{})); + fail(); + } + catch(std::exception const&) + { + pass(); + } + BEAST_EXPECT(!sig.slot().has_handler()); + BEAST_EXPECT(! sh.has_value()); + } + } + void run() override { testSavedHandler(); + testSavedHandlerCancellation(); } }; diff --git a/test/beast/http/read.cpp b/test/beast/http/read.cpp index 719eea6f..23439abc 100644 --- a/test/beast/http/read.cpp +++ b/test/beast/http/read.cpp @@ -25,6 +25,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #if BOOST_ASIO_HAS_CO_AWAIT @@ -729,7 +735,57 @@ public: } } - + void + testCancellation(yield_context do_yield) + { + // this is tested on a pipe + // because the test::stream doesn't implement cancellation + { + response m; + error_code ec; + net::writable_pipe ts{ioc_}; + net::readable_pipe tr{ioc_}; + net::connect_pipe(tr, ts); + net::cancellation_signal cl; + net::post(ioc_, [&]{cl.emit(net::cancellation_type::all);}); + net::steady_timer timeout(ioc_, std::chrono::seconds(5)); + timeout.async_wait( + [&](error_code ec) + { + BEAST_EXPECT(ec == net::error::operation_aborted); + if (!ec) // this means the cancel failed! + ts.close(); + }); + multi_buffer b; + async_read(tr, b, m, net::bind_cancellation_slot(cl.slot(), do_yield[ec])); + timeout.cancel(); + BEAST_EXPECT(ec == net::error::operation_aborted); + } + { + response m; + error_code ec; + net::writable_pipe ts{ioc_}; + net::readable_pipe tr{ioc_}; + net::connect_pipe(tr, ts); + net::cancellation_signal cl; + net::post(ioc_, [&]{cl.emit(net::cancellation_type::all);}); + net::steady_timer timeout(ioc_, std::chrono::seconds(5)); + timeout.async_wait( + [&](error_code ec) + { + // using BEAST_EXPECT HERE is a race condition, since the test suite might + BEAST_EXPECT(ec == net::error::operation_aborted); + if (!ec) // this means the cancel failed! + ts.close(); + }); + multi_buffer b; + async_read(tr, b, m, net::bind_cancellation_slot(cl.slot(), do_yield[ec])); + timeout.cancel(); + BEAST_EXPECT(ec == net::error::operation_aborted); + } + // the timer handler may be invoked after the test suite is complete if we don't post. + asio::post(ioc_, do_yield); + } void run() override { @@ -761,6 +817,11 @@ public: testReadSomeHeader(yield); }); testReadSomeHeader(); + yield_to( + [&](yield_context yield) + { + testCancellation(yield); + }); } diff --git a/test/beast/http/write.cpp b/test/beast/http/write.cpp index 81454383..a8a618a4 100644 --- a/test/beast/http/write.cpp +++ b/test/beast/http/write.cpp @@ -22,8 +22,13 @@ #include #include #include +#include #include #include +#include +#include +#include +#include #include #include #include @@ -1050,6 +1055,69 @@ public: } #endif + void + testCancellation(yield_context do_yield) + { + // this is tested on a pipe + // because the test::stream doesn't implement cancellation + { + response m; + m.version(10); + m.result(status::ok); + m.set(field::server, "test"); + m.set(field::content_length, "5"); + // make the content big enough so it overflows the buffer + // that'll make the op never complete if we don't cancel + m.body().assign(10000000, '*'); + error_code ec; + net::writable_pipe ts{ioc_}; + net::readable_pipe tr{ioc_}; + net::connect_pipe(tr, ts); + net::cancellation_signal cl; + net::post(ioc_, [&]{cl.emit(net::cancellation_type::all);}); + net::steady_timer timeout(ioc_, std::chrono::seconds(5)); + timeout.async_wait( + [&](error_code ec) + { + BEAST_EXPECT(ec == net::error::operation_aborted); + if (!ec) // this means the cancel failed! + ts.close(); + }); + + async_write(ts, m, net::bind_cancellation_slot(cl.slot(), do_yield[ec])); + timeout.cancel(); + net::post(ioc_, do_yield); // wait for the timeout to finish + BEAST_EXPECT(ec == net::error::operation_aborted); + } + { + response m; + m.version(11); + m.result(status::ok); + m.set(field::server, "test"); + m.set(field::transfer_encoding, "chunked"); + m.body().assign(10000000, '*'); + error_code ec; + net::writable_pipe ts{ioc_}; + net::readable_pipe tr{ioc_}; + net::connect_pipe(tr, ts); + net::cancellation_signal cl; + net::post(ioc_, [&]{cl.emit(net::cancellation_type::all);}); + net::steady_timer timeout(ioc_, std::chrono::seconds(5)); + timeout.async_wait( + [&](error_code ec) + { + BEAST_EXPECT(ec == net::error::operation_aborted); + if (!ec) // this means the cancel failed! + ts.close(); + }); + async_write(ts, m, net::bind_cancellation_slot(cl.slot(), do_yield[ec])); + timeout.cancel(); + net::post(ioc_, do_yield); // wait for the timeout to finish + BEAST_EXPECT(ec == net::error::operation_aborted); + } + // the timer handler may be invoked after the test suite is complete if we don't post. + asio::post(ioc_, do_yield); + } void run() override @@ -1077,6 +1145,11 @@ public: #if BOOST_ASIO_HAS_CO_AWAIT boost::ignore_unused(&write_test::testAwaitableCompiles); #endif + yield_to( + [&](yield_context yield) + { + testCancellation(yield); + }); } }; diff --git a/test/beast/websocket/CMakeLists.txt b/test/beast/websocket/CMakeLists.txt index ee7a5665..ebece393 100644 --- a/test/beast/websocket/CMakeLists.txt +++ b/test/beast/websocket/CMakeLists.txt @@ -21,6 +21,7 @@ add_executable (tests-beast-websocket test.hpp _detail_prng.cpp accept.cpp + cancel.cpp close.cpp error.cpp frame.cpp diff --git a/test/beast/websocket/Jamfile b/test/beast/websocket/Jamfile index 1469d861..958d5ff0 100644 --- a/test/beast/websocket/Jamfile +++ b/test/beast/websocket/Jamfile @@ -12,6 +12,7 @@ local SOURCES = _detail_impl_base.cpp _detail_prng.cpp accept.cpp + cancel.cpp close.cpp error.cpp frame.cpp diff --git a/test/beast/websocket/cancel.cpp b/test/beast/websocket/cancel.cpp new file mode 100644 index 00000000..5933620a --- /dev/null +++ b/test/beast/websocket/cancel.cpp @@ -0,0 +1,230 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +// Test that header file is self-contained. +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test.hpp" + +namespace boost { +namespace beast { +namespace websocket { + +struct async_all_server_op : boost::asio::coroutine +{ + stream & ws; + + async_all_server_op(stream & ws) : ws(ws) {} + + template + void operator()(Self && self, error_code ec = {}, std::size_t sz = 0) + { + if (ec) + return self.complete(ec); + + BOOST_ASIO_CORO_REENTER(*this) + { + self.reset_cancellation_state([](net::cancellation_type ct){return ct;}); + BOOST_ASIO_CORO_YIELD ws.async_handshake("test", "/", std::move(self)); + BOOST_ASIO_CORO_YIELD ws.async_ping("", std::move(self)); + BOOST_ASIO_CORO_YIELD ws.async_write_some(false, net::buffer("FOO", 3), std::move(self)); + BOOST_ASIO_CORO_YIELD ws.async_write_some(true, net::buffer("BAR", 3), std::move(self)); + BOOST_ASIO_CORO_YIELD ws.async_close("testing", std::move(self)); + self.complete({}); + } + } +}; + + +template +BOOST_BEAST_ASYNC_RESULT1(CompletionToken) +async_all_server( + stream & ws, + CompletionToken && token) +{ + return net::async_compose + ( + async_all_server_op{ws}, + token, ws + ); +} + + +struct async_all_client_op : boost::asio::coroutine +{ + stream & ws; + + async_all_client_op(stream & ws) : ws(ws) {} + struct impl_t + { + impl_t () = default; + std::string res; + net::dynamic_string_buffer, std::allocator> buf = + net::dynamic_buffer(res); + }; + + std::shared_ptr impl{std::make_shared()}; + + template + void operator()(Self && self, error_code ec = {}, std::size_t sz = 0) + { + if (ec) + return self.complete(ec); + BOOST_ASIO_CORO_REENTER(*this) + { + // let everything pass + self.reset_cancellation_state([](net::cancellation_type ct){return ct;}); + + BOOST_ASIO_CORO_YIELD ws.async_accept(std::move(self)); + BOOST_ASIO_CORO_YIELD ws.async_pong("", std::move(self)); + + + BOOST_ASIO_CORO_YIELD ws.async_read(impl->buf, std::move(self)); + BEAST_EXPECTS(impl->res == "FOOBAR", impl->res); + + BOOST_ASIO_CORO_YIELD ws.async_read(impl->buf, std::move(self)); + BEAST_EXPECTS(ec == websocket::error::closed + || ec == net::error::connection_reset + // hard coded winapi error (WSAECONNRESET), same as connection_reset, but asio delivers it with system_category + || (ec.value() == 10054 + && ec.category() == system::system_category()) + || ec == net::error::not_connected, ec.message()); + self.complete({}); + + } + + } +}; + +template +BOOST_BEAST_ASYNC_RESULT1(CompletionToken) +async_all_client( + stream & ws, + CompletionToken && token) +{ + return net::async_compose + ( + async_all_client_op{ws}, + token, ws + ); +} + +class cancel_test : public websocket_test_suite +{ +public: + std::size_t run_impl(net::cancellation_signal &sl, + net::io_context &ctx, + std::size_t trigger = -1, + net::cancellation_type tp = net::cancellation_type::terminal) + { + std::size_t cnt = 0; + std::size_t res = 0u; + while ((res = ctx.run_one()) != 0) + if (trigger == cnt ++) + { + sl.emit(tp); + } + + return cnt; + } + + std::size_t testAll(std::size_t & cancel_counter, + bool cancel_server = false, + std::size_t trigger = -1) + { + net::cancellation_signal sig1, sig2; + + net::io_context ioc; + using tcp = net::ip::tcp; + + stream ws1(ioc.get_executor()); + stream ws2(ioc.get_executor()); + test::connect(ws1.next_layer(), ws2.next_layer()); + + async_all_server(ws1, + net::bind_cancellation_slot(sig1.slot(), [&](system::error_code ec) + { + if (ec) + { + if (ec == net::error::operation_aborted && + (cancel_server || trigger == static_cast(-1))) + cancel_counter++; + + BEAST_EXPECTS(ec == net::error::operation_aborted + || ec == net::error::broken_pipe + // winapi WSAECONNRESET, as system_category + || ec == error_code(10054, boost::system::system_category()) + || ec == net::error::bad_descriptor + || ec == net::error::eof, ec.message()); + get_lowest_layer(ws1).close(); + } + })); + + async_all_client( + ws2, + net::bind_cancellation_slot(sig2.slot(), [&](system::error_code ec) + { + if (ec) + { + if (ec == net::error::operation_aborted && + (!cancel_server || trigger == static_cast(-1))) + cancel_counter++; + BEAST_EXPECTS(ec == net::error::operation_aborted + || ec == error::closed + || ec == net::error::broken_pipe + || ec == net::error::connection_reset + || ec == net::error::not_connected + // winapi WSAECONNRESET, as system_category + || ec == error_code(10054, boost::system::system_category()) + || ec == net::error::eof, ec.message()); + get_lowest_layer(ws1).close(); + } + })); + + return run_impl(cancel_server ? sig1 : sig2, ioc, trigger); + } + + void brute_force() + { + std::size_t cancel_counter = 0; + const auto init = testAll(cancel_counter); + BEAST_EXPECT(cancel_counter == 0u); + for (std::size_t cnt = 0; cnt < init; cnt ++) + testAll(cancel_counter, true, cnt); + + BEAST_EXPECT(cancel_counter > 0u); + cancel_counter = 0u; + for (std::size_t cnt = 0; cnt < init; cnt ++) + testAll(cancel_counter, false, cnt); + + + BEAST_EXPECT(cancel_counter > 0u); + } + + void + run() override + { + brute_force(); + } +}; + +BEAST_DEFINE_TESTSUITE(beast,websocket,cancel); + +} // websocket +} // beast +} // boost diff --git a/test/doc/core_1_refresher.cpp b/test/doc/core_1_refresher.cpp index 02f7610e..a42c7551 100644 --- a/test/doc/core_1_refresher.cpp +++ b/test/doc/core_1_refresher.cpp @@ -251,6 +251,9 @@ struct handler using executor_type = boost::asio::io_context::executor_type; executor_type get_executor() const noexcept; + using cancellation_slot_type = boost::asio::cancellation_slot; + cancellation_slot_type get_cancellation_slot() const noexcept; + void operator()(boost::beast::error_code, std::size_t); }; //] @@ -265,6 +268,12 @@ inline auto handler::get_executor() const noexcept -> static boost::asio::io_context ioc; return ioc.get_executor(); } +inline auto handler::get_cancellation_slot() const noexcept -> + cancellation_slot_type +{ + return cancellation_slot_type(); +} + inline void handler::operator()( boost::beast::error_code, std::size_t) { @@ -296,6 +305,18 @@ struct associated_executor Executor const& ex = Executor{}) noexcept; }; +template +struct associated_cancellation_slot +{ + using type = cancellation_slot; + + static + type + get(handler const& h, + CancellationSlot const& cs = CancellationSlot{}) noexcept; +}; + + } // boost } // asio //] @@ -315,6 +336,15 @@ get(handler const&, Executor const&) noexcept -> type return {}; } +template +auto +boost::asio::associated_cancellation_slot:: +get(handler const&, CancellationSlot const&) noexcept -> type +{ + return {}; +} + + //------------------------------------------------------------------------------ namespace boost {