Gracefully shutdown Websocket and TLS streams

Summary:
Resolves #18
Ref T15241

Reviewers: ivica

Reviewed By: ivica

Subscribers: korina, miljen

Maniphest Tasks: T15241

Differential Revision: https://repo.mireo.local/D33395
This commit is contained in:
Bruno Iljazovic
2025-01-30 18:16:30 +01:00
parent 4df9dbbb07
commit 8b41f0dd4d
23 changed files with 460 additions and 59 deletions

View File

@ -95,7 +95,6 @@ public:
void close() {
error_code ec;
detail::lowest_layer(*_stream_ptr).shutdown(asio::ip::tcp::socket::shutdown_both, ec);
detail::lowest_layer(*_stream_ptr).close(ec);
}

View File

@ -224,6 +224,7 @@ public:
auto initiation = [this](
auto handler, const MutableBuffer& buffer
) {
_pending_read.complete(_ex, asio::error::operation_aborted, 0);
_pending_read = pending_read(buffer, std::move(handler));
complete_read();
};

View File

@ -79,10 +79,6 @@ public:
_test_broker = nullptr;
}
void shutdown(asio::ip::tcp::socket::shutdown_type, error_code& ec) {
ec = {};
}
void connect(const endpoint_type& ep, error_code& ec) {
ec = {};
_remote_ep = ep;
@ -165,8 +161,6 @@ public:
}
void operator()(on_read, error_code ec, size_t bytes_read) {
if (ec)
_stream_impl->disconnect();
complete(ec, bytes_read);
}
@ -223,8 +217,6 @@ public:
}
void operator()(on_write, error_code ec, size_t bytes_written) {
if (ec)
_stream_impl->disconnect();
complete(ec, bytes_written);
}
@ -300,10 +292,6 @@ public:
return _impl->is_connected();
}
void shutdown(asio::ip::tcp::socket::shutdown_type st, error_code& ec) {
return _impl->shutdown(st, ec);
}
endpoint_type remote_endpoint(error_code& ec) {
return _impl->remote_endpoint(ec);
}
@ -368,6 +356,10 @@ public:
};
template <typename ShutdownHandler>
void async_shutdown(test_stream&, ShutdownHandler&& handler) {
return std::move(handler)(error_code {});
}
} // end namespace boost::mqtt5::test

View File

@ -11,7 +11,7 @@
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/mqtt5.hpp>
#include <boost/mqtt5/websocket.hpp>
#include <boost/mqtt5/websocket_ssl.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/co_spawn.hpp>
@ -19,7 +19,6 @@
#include <boost/asio/io_context.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/ssl/ssl_stream.hpp> // async_teardown specialization for websocket ssl stream
#include <boost/beast/websocket.hpp>
#include <boost/system/error_code.hpp>

View File

@ -11,6 +11,7 @@
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/test/data/test_case.hpp>
#include <boost/test/unit_test.hpp>
#include <chrono>
@ -311,4 +312,67 @@ BOOST_FIXTURE_TEST_CASE(omit_props, shared_test_data) {
BOOST_TEST(handlers_called == expected_handlers_called);
}
struct long_shutdown_stream : public test::test_stream {
long_shutdown_stream(typename test::test_stream::executor_type ex) :
test::test_stream(std::move(ex)) {}
};
template <typename ShutdownHandler>
void async_shutdown(long_shutdown_stream& stream, ShutdownHandler&& handler) {
auto timer = std::make_shared<asio::steady_timer>(stream.get_executor());
timer->expires_after(std::chrono::seconds(10));
timer->async_wait(asio::consign(std::move(handler), std::move(timer)));
}
BOOST_DATA_TEST_CASE_F(
shared_test_data, cancel_disconnect_in_shutdown,
boost::unit_test::data::make({ 100, 8000 }), cancel_delay_ms
) {
asio::io_context ioc;
auto executor = ioc.get_executor();
constexpr int expected_handlers_called = 1;
int handlers_called = 0;
test::msg_exchange broker_side;
broker_side
.expect(connect)
.complete_with(success, after(0ms))
.reply_with(connack, after(0ms))
.expect(disconnect)
.complete_with(success, after(0ms));
auto& broker = asio::make_service<test::test_broker>(
ioc, executor, std::move(broker_side)
);
asio::steady_timer timer(executor);
mqtt_client<long_shutdown_stream> c(executor);
c.brokers("127.0.0.1")
.async_run(asio::detached);
asio::cancellation_signal signal;
c.async_disconnect(
asio::bind_cancellation_slot(
signal.slot(),
[&](error_code ec) {
handlers_called++;
BOOST_TEST(ec == asio::error::operation_aborted);
timer.cancel();
}
)
);
timer.expires_after(std::chrono::milliseconds(cancel_delay_ms));
timer.async_wait([&signal](error_code) {
signal.emit(asio::cancellation_type::all);
});
ioc.run_for(6s);
BOOST_TEST(broker.received_all_expected());
BOOST_TEST(handlers_called == expected_handlers_called);
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -172,6 +172,52 @@ BOOST_FIXTURE_TEST_CASE(receive_disconnect, shared_test_data) {
BOOST_TEST(broker.received_all_expected());
}
BOOST_FIXTURE_TEST_CASE(receive_disconnect_while_reconnecting, shared_test_data) {
// packets
auto disconnect = encoders::encode_disconnect(0x00, {});
constexpr int expected_handlers_called = 1;
int handlers_called = 0;
test::msg_exchange broker_side;
broker_side
.expect(connect)
.complete_with(success, after(1ms))
.reply_with(connack, after(2ms))
.expect(publish)
.complete_with(fail, after(20ms))
.send(disconnect, after(30ms))
.expect(connect)
.complete_with(success, after(20ms))
.reply_with(connack, after(30ms))
.expect(publish)
.complete_with(success, after(0ms));
asio::io_context ioc;
auto executor = ioc.get_executor();
auto& broker = asio::make_service<test::test_broker>(
ioc, executor, std::move(broker_side)
);
using client_type = mqtt_client<test::test_stream>;
client_type c(executor);
c.brokers("127.0.0.1,127.0.0.1") // to avoid reconnect backoff
.async_run(asio::detached);
c.async_publish<qos_e::at_most_once>(
topic, payload, retain_e::no, {},
[&](error_code ec) {
BOOST_TEST(handlers_called == 0);
handlers_called++;
BOOST_TEST(!ec);
c.cancel();
}
);
ioc.run_for(1s);
BOOST_TEST(handlers_called == expected_handlers_called);
BOOST_TEST(broker.received_all_expected());
}
template <typename VerifyFun>
void run_receive_test(
test::msg_exchange broker_side, int num_of_receives,

View File

@ -533,7 +533,7 @@ BOOST_FIXTURE_TEST_CASE(cancel_resending_publish, shared_test_data) {
[&handlers_called, &c](error_code ec, reason_code rc, puback_props) {
++handlers_called;
BOOST_TEST(ec = asio::error::operation_aborted);
BOOST_TEST(ec == asio::error::operation_aborted);
BOOST_TEST(rc == reason_codes::empty);
c.cancel();

View File

@ -11,13 +11,12 @@
#ifdef BOOST_ASIO_HAS_CO_AWAIT
#include <boost/mqtt5.hpp>
#include <boost/mqtt5/websocket.hpp>
#include <boost/mqtt5/websocket_ssl.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/websocket/ssl.hpp> // async_teardown for asio::ssl::socket
#include <boost/beast/websocket/stream.hpp>
#include <cstdint>

View File

@ -8,7 +8,7 @@
#include <boost/mqtt5/logger.hpp>
#include <boost/mqtt5/logger_traits.hpp>
#include <boost/mqtt5/mqtt_client.hpp>
#include <boost/mqtt5/websocket.hpp>
#include <boost/mqtt5/websocket_ssl.hpp>
#include <boost/mqtt5/detail/log_invoke.hpp>
@ -16,7 +16,6 @@
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/ssl/ssl_stream.hpp> // async_teardown specialization for websocket ssl stream
#include <boost/beast/websocket/stream.hpp>
#include <boost/test/tools/output_test_stream.hpp>
#include <boost/test/unit_test.hpp>

View File

@ -84,6 +84,11 @@ struct test_tcp_stream : public test::test_stream {
}
};
template <typename ShutdownHandler>
void async_shutdown(test_tcp_stream&, ShutdownHandler&& handler) {
return std::move(handler)(error_code {});
}
using underlying_stream = test_tcp_stream;
using stream_context = detail::stream_context<underlying_stream, std::monostate>;
using astream = test::test_autoconnect_stream<underlying_stream, stream_context>;