diff --git a/README.md b/README.md index 25e5817b..608c9b74 100644 --- a/README.md +++ b/README.md @@ -693,6 +693,11 @@ https://lists.boost.org/Archives/boost/2023/01/253944.php. apps need only one connection for their entire application, which makes the overhead of one ssl-context per connection negligible. +* ([Issue 181](https://github.com/boostorg/redis/issues/181)). + See a detailed description of this bug in + [this](https://github.com/boostorg/redis/issues/181#issuecomment-1913346983) + comment. + ### Boost 1.84 (First release in Boost) * Deprecates the `async_receive` overload that takes a response. Users diff --git a/include/boost/redis/detail/connection_base.hpp b/include/boost/redis/detail/connection_base.hpp index 043203bc..69ae68ca 100644 --- a/include/boost/redis/detail/connection_base.hpp +++ b/include/boost/redis/detail/connection_base.hpp @@ -506,6 +506,9 @@ class connection_base { usage get_usage() const noexcept { return usage_; } + auto run_is_canceled() const noexcept + { return cancel_run_called_; } + private: using receive_channel_type = asio::experimental::channel; using runner_type = runner; @@ -539,6 +542,7 @@ class connection_base { }); reqs_.erase(point, std::end(reqs_)); + std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) { return ptr->mark_waiting(); }); @@ -575,6 +579,12 @@ class connection_base { } break; case operation::run: { + // Protects the code below from being called more than + // once, see https://github.com/boostorg/redis/issues/181 + if (std::exchange(cancel_run_called_, true)) { + return; + } + close(); writer_timer_.cancel(); receive_channel_.cancel(); @@ -601,8 +611,9 @@ class connection_base { // partition of unwritten requests instead of them all. std::for_each(std::begin(reqs_), std::end(reqs_), [](auto const& ptr) { BOOST_ASSERT_MSG(ptr != nullptr, "Expects non-null pointer."); - if (ptr->is_staged()) + if (ptr->is_staged()) { ptr->mark_written(); + } }); } @@ -921,6 +932,7 @@ class connection_base { read_buffer_.clear(); parser_.reset(); on_push_ = false; + cancel_run_called_ = false; } asio::ssl::context ctx_; @@ -942,6 +954,7 @@ class connection_base { reqs_type reqs_; resp3::parser parser_{}; bool on_push_ = false; + bool cancel_run_called_ = false; usage usage_; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 40edb275..049469f3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -47,6 +47,7 @@ make_test(test_conn_exec_cancel2 20) make_test(test_conn_echo_stress 20) make_test(test_conn_run_cancel 20) make_test(test_issue_50 20) +make_test(test_issue_181 17) # Coverage set( diff --git a/test/test_issue_181.cpp b/test/test_issue_181.cpp new file mode 100644 index 00000000..a407aa71 --- /dev/null +++ b/test/test_issue_181.cpp @@ -0,0 +1,64 @@ +/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include +#define BOOST_TEST_MODULE conn-quit +#include +#include +#include +#include +#include +#include +#include "common.hpp" + +namespace net = boost::asio; +using boost::redis::request; +using boost::redis::request; +using boost::redis::response; +using boost::redis::ignore; +using boost::redis::logger; +using boost::redis::config; +using boost::redis::operation; +using boost::redis::connection; +using boost::system::error_code; +using namespace std::chrono_literals; + +BOOST_AUTO_TEST_CASE(issue_181) +{ + using connection_base = boost::redis::detail::connection_base; + + auto const level = boost::redis::logger::level::debug; + net::io_context ioc; + auto ctx = net::ssl::context{net::ssl::context::tlsv12_client}; + connection_base conn{ioc.get_executor(), std::move(ctx), 1000000}; + net::steady_timer timer{ioc}; + timer.expires_after(std::chrono::seconds{1}); + + auto run_cont = [&](auto ec){ + std::cout << "async_run1: " << ec.message() << std::endl; + }; + + boost::redis::config cfg; + cfg.health_check_interval = std::chrono::seconds{0}; + cfg.reconnect_wait_interval = std::chrono::seconds{0}; + conn.async_run(cfg, boost::redis::logger{level}, run_cont); + BOOST_TEST(!conn.run_is_canceled()); + + // Uses a timer to wait some time until run has been called. + auto timer_cont = [&](auto ec){ + std::cout << "timer_cont: " << ec.message() << std::endl; + BOOST_TEST(!conn.run_is_canceled()); + conn.cancel(operation::run); + BOOST_TEST(conn.run_is_canceled()); + }; + + timer.async_wait(timer_cont); + + ioc.run(); +}