diff --git a/include/boost/redis/adapter/any_adapter.hpp b/include/boost/redis/adapter/any_adapter.hpp new file mode 100644 index 00000000..bf14d6e5 --- /dev/null +++ b/include/boost/redis/adapter/any_adapter.hpp @@ -0,0 +1,80 @@ +/* Copyright (c) 2018-2023 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_ANY_ADAPTER_HPP +#define BOOST_REDIS_ANY_ADAPTER_HPP + + +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis { + +namespace detail { + +// Forward decl +template +class connection_base; + +} + +/** @brief A type-erased reference to a response. + * @ingroup high-level-api + * + * A type-erased response adapter. It can be executed using @ref connection::async_exec. + * Using this type instead of raw response references enables separate compilation. + * + * Given a response object `resp` that can be passed to `async_exec`, the following two + * statements have the same effect: + * ``` + * co_await conn.async_exec(req, resp); + * co_await conn.async_exec(req, any_response(resp)); + * ``` + */ +class any_adapter +{ + using fn_type = std::function const&, system::error_code&)>; + + struct impl_t { + fn_type adapt_fn; + std::size_t supported_response_size; + } impl_; + + template + static auto create_impl(T& resp) -> impl_t + { + using namespace boost::redis::adapter; + auto adapter = boost_redis_adapt(resp); + std::size_t size = adapter.get_supported_response_size(); + return { std::move(adapter), size }; + } + + template + friend class detail::connection_base; + +public: + /** + * @brief Constructor. + * + * Creates a type-erased response adapter from `resp` by calling + * `boost_redis_adapt`. `T` must be a valid Redis response type. + * Any type passed to @ref connection::async_exec qualifies. + * + * This object stores a reference to `resp`, which must be kept alive + * while `*this` is being used. + */ + template >> + explicit any_adapter(T& resp) : impl_(create_impl(resp)) {} +}; + +} + +#endif diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index 71348f33..501adc82 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -7,6 +7,7 @@ #ifndef BOOST_REDIS_CONNECTION_HPP #define BOOST_REDIS_CONNECTION_HPP +#include #include #include #include @@ -17,7 +18,7 @@ #include #include -#include +#include #include namespace boost::redis { @@ -256,7 +257,22 @@ class basic_connection { Response& resp = ignore, CompletionToken&& token = CompletionToken{}) { - return impl_.async_exec(req, resp, std::forward(token)); + return impl_.async_exec(req, any_adapter(resp), std::forward(token)); + } + + /** @copydoc async_exec + * + * @details This function uses the type-erased @ref any_adapter class, which + * encapsulates a reference to a response object. + */ + template > + auto + async_exec( + request const& req, + any_adapter adapter, + CompletionToken&& token = CompletionToken{}) + { + return impl_.async_exec(req, std::move(adapter), std::forward(token)); } /** @brief Cancel operations. @@ -392,9 +408,21 @@ class connection { /// Calls `boost::redis::basic_connection::async_exec`. template - auto async_exec(request const& req, Response& resp, CompletionToken token) + auto async_exec(request const& req, Response& resp, CompletionToken&& token) { - return impl_.async_exec(req, resp, std::move(token)); + return async_exec(req, any_adapter(resp), std::forward(token)); + } + + /// Calls `boost::redis::basic_connection::async_exec`. + template + auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token) + { + return asio::async_initiate< + CompletionToken, void(boost::system::error_code, std::size_t)>( + [](auto handler, connection* self, request const* req, any_adapter&& adapter) + { + self->async_exec_impl(*req, std::move(adapter), std::move(handler)); + }, token, this, &req, std::move(adapter)); } /// Calls `boost::redis::basic_connection::cancel`. @@ -435,6 +463,12 @@ class connection { config const& cfg, logger l, asio::any_completion_handler token); + + void + async_exec_impl( + request const& req, + any_adapter&& adapter, + asio::any_completion_handler token); basic_connection impl_; }; diff --git a/include/boost/redis/detail/connection_base.hpp b/include/boost/redis/detail/connection_base.hpp index 487c759f..46cd6b78 100644 --- a/include/boost/redis/detail/connection_base.hpp +++ b/include/boost/redis/detail/connection_base.hpp @@ -7,6 +7,7 @@ #ifndef BOOST_REDIS_CONNECTION_BASE_HPP #define BOOST_REDIS_CONNECTION_BASE_HPP +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include @@ -39,6 +41,7 @@ #include #include #include +#include namespace boost::redis::detail { @@ -121,7 +124,9 @@ struct exec_op { // be stablished. if (info_->req_->get_config().cancel_if_not_connected && !conn_->is_open()) { BOOST_ASIO_CORO_YIELD - asio::post(std::move(self)); + asio::dispatch( + asio::get_associated_immediate_executor(self, self.get_io_executor()), + std::move(self)); return self.complete(error::not_connected, 0); } @@ -440,14 +445,13 @@ class connection_base { cancel_impl(op); } - template - auto async_exec(request const& req, Response& resp, CompletionToken token) + template + auto async_exec(request const& req, any_adapter&& adapter, CompletionToken&& token) { - using namespace boost::redis::adapter; - auto f = boost_redis_adapt(resp); - BOOST_ASSERT_MSG(req.get_expected_responses() <= f.get_supported_response_size(), "Request and response have incompatible sizes."); + auto& adapter_impl = adapter.impl_; + BOOST_ASSERT_MSG(req.get_expected_responses() <= adapter_impl.supported_response_size, "Request and response have incompatible sizes."); - auto info = std::make_shared(req, f, get_executor()); + auto info = std::make_shared(req, std::move(adapter_impl.adapt_fn), get_executor()); return asio::async_compose < CompletionToken diff --git a/include/boost/redis/detail/health_checker.hpp b/include/boost/redis/detail/health_checker.hpp index 6d62a1d4..db910718 100644 --- a/include/boost/redis/detail/health_checker.hpp +++ b/include/boost/redis/detail/health_checker.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,7 @@ class ping_op { } BOOST_ASIO_CORO_YIELD - conn_->async_exec(checker_->req_, checker_->resp_, std::move(self)); + conn_->async_exec(checker_->req_, any_adapter(checker_->resp_), std::move(self)); if (ec || is_cancelled(self)) { logger_.trace("ping_op: error/cancelled (1)."); checker_->wait_timer_.cancel(); diff --git a/include/boost/redis/detail/runner.hpp b/include/boost/redis/detail/runner.hpp index f257019b..202bc1e0 100644 --- a/include/boost/redis/detail/runner.hpp +++ b/include/boost/redis/detail/runner.hpp @@ -7,6 +7,7 @@ #ifndef BOOST_REDIS_RUNNER_HPP #define BOOST_REDIS_RUNNER_HPP +#include #include #include #include @@ -47,7 +48,7 @@ struct hello_op { runner_->add_hello(); BOOST_ASIO_CORO_YIELD - conn_->async_exec(runner_->hello_req_, runner_->hello_resp_, std::move(self)); + conn_->async_exec(runner_->hello_req_, any_adapter(runner_->hello_resp_), std::move(self)); logger_.on_hello(ec, runner_->hello_resp_); if (ec || runner_->has_error_in_response() || is_cancelled(self)) { diff --git a/include/boost/redis/impl/connection.ipp b/include/boost/redis/impl/connection.ipp index 12abc996..1139d33e 100644 --- a/include/boost/redis/impl/connection.ipp +++ b/include/boost/redis/impl/connection.ipp @@ -5,6 +5,7 @@ */ #include +#include namespace boost::redis { @@ -31,6 +32,15 @@ connection::async_run_impl( impl_.async_run(cfg, l, std::move(token)); } +void +connection::async_exec_impl( + request const& req, + any_adapter&& adapter, + asio::any_completion_handler token) +{ + impl_.async_exec(req, std::move(adapter), std::move(token)); +} + void connection::cancel(operation op) { impl_.cancel(op); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d5fc0793..14dcc0ca 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -47,6 +47,7 @@ make_test(test_conn_exec_cancel 20) make_test(test_conn_exec_cancel2 20) make_test(test_conn_echo_stress 20) make_test(test_conn_run_cancel 20) +make_test(test_any_adapter 17) make_test(test_issue_50 20) make_test(test_issue_181 17) diff --git a/test/Jamfile b/test/Jamfile index 81e1fd72..9292d8c6 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -49,6 +49,7 @@ local tests = test_low_level test_request test_run + test_any_adapter ; # Build and run the tests diff --git a/test/test_any_adapter.cpp b/test/test_any_adapter.cpp new file mode 100644 index 00000000..12e5d44d --- /dev/null +++ b/test/test_any_adapter.cpp @@ -0,0 +1,49 @@ +/* Copyright (c) 2018-2022 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 any_adapter +#include + +using boost::redis::generic_response; +using boost::redis::response; +using boost::redis::ignore; +using boost::redis::any_adapter; + +BOOST_AUTO_TEST_CASE(any_adapter_response_types) +{ + // any_adapter can be used with any supported responses + response r1; + response r2; + generic_response r3; + + BOOST_CHECK_NO_THROW(any_adapter{r1}); + BOOST_CHECK_NO_THROW(any_adapter{r2}); + BOOST_CHECK_NO_THROW(any_adapter{r3}); + BOOST_CHECK_NO_THROW(any_adapter{ignore}); +} + +BOOST_AUTO_TEST_CASE(any_adapter_copy_move) +{ + // any_adapter can be copied/moved + response r; + any_adapter ad1 {r}; + + // copy constructor + any_adapter ad2 {ad1}; + + // move constructor + any_adapter ad3 {std::move(ad2)}; + + // copy assignment + BOOST_CHECK_NO_THROW(ad2 = ad1); + + // move assignment + BOOST_CHECK_NO_THROW(ad2 = std::move(ad1)); +} diff --git a/test/test_conn_exec.cpp b/test/test_conn_exec.cpp index c3f04134..43982639 100644 --- a/test/test_conn_exec.cpp +++ b/test/test_conn_exec.cpp @@ -4,9 +4,11 @@ * accompanying file LICENSE.txt) */ +#include #include #include #include +#include #define BOOST_TEST_MODULE conn-exec #include #include @@ -188,3 +190,25 @@ BOOST_AUTO_TEST_CASE(large_number_of_concurrent_requests_issue_170) BOOST_CHECK_EQUAL(counter, repeat); } +BOOST_AUTO_TEST_CASE(exec_any_response) +{ + // Executing an any_response object works + request req; + req.push("PING", "PONG"); + response res; + + net::io_context ioc; + + auto conn = std::make_shared(ioc); + + conn->async_exec(req, boost::redis::any_adapter(res), [&](auto ec, auto){ + BOOST_TEST(!ec); + conn->cancel(); + }); + + run(conn); + ioc.run(); + + BOOST_TEST(std::get<0>(res).value() == "PONG"); +} +