From a6cb4ca323f15ea647634b0319c0c13fb6a88334 Mon Sep 17 00:00:00 2001 From: Marcelo Zimbres Date: Fri, 24 Mar 2023 19:55:15 +0100 Subject: [PATCH] Adds high-level functionality to connection::async_run. --- CMakeLists.txt | 53 +- README.md | 185 ++----- examples/cpp17_intro.cpp | 34 +- examples/cpp17_intro_sync.cpp | 68 +-- examples/cpp20_chat_room.cpp | 73 ++- examples/cpp20_containers.cpp | 51 +- examples/cpp20_echo_server.cpp | 49 +- examples/cpp20_intro.cpp | 42 +- examples/cpp20_intro_awaitable_ops.cpp | 43 -- examples/cpp20_intro_tls.cpp | 57 +-- examples/cpp20_json.cpp | 33 +- examples/cpp20_protobuf.cpp | 32 +- examples/cpp20_resolve_with_sentinel.cpp | 31 +- examples/cpp20_subscriber.cpp | 63 ++- examples/main.cpp | 14 +- examples/start.cpp | 2 +- examples/sync_connection.hpp | 63 +++ include/boost/redis.hpp | 3 + .../boost/redis/adapter/detail/adapters.hpp | 1 - .../redis/adapter/detail/result_traits.hpp | 1 + include/boost/redis/address.hpp | 27 - include/boost/redis/config.hpp | 72 +++ include/boost/redis/connection.hpp | 168 +++--- .../boost/redis/detail/connection_base.hpp | 482 ++++++++++++++++-- include/boost/redis/detail/connection_ops.hpp | 403 --------------- include/boost/redis/detail/connector.hpp | 133 +++++ .../health_checker.hpp} | 94 ++-- include/boost/redis/detail/reconnection.hpp | 135 +++++ include/boost/redis/detail/resolver.hpp | 137 +++++ include/boost/redis/detail/runner.hpp | 383 +++++++------- include/boost/redis/error.hpp | 3 + .../boost/redis/experimental/connector.hpp | 313 ------------ include/boost/redis/impl/logger.ipp | 128 +++++ include/boost/redis/logger.hpp | 126 ++++- include/boost/redis/operation.hpp | 14 +- include/boost/redis/request.hpp | 24 +- .../boost/redis/resp3/impl/serialization.ipp | 1 - include/boost/redis/resp3/serialization.hpp | 1 - include/boost/redis/run.hpp | 65 --- include/boost/redis/src.hpp | 1 + include/boost/redis/ssl/connection.hpp | 82 ++- include/boost/redis/ssl/detail/handshaker.hpp | 124 +++++ tests/common.cpp | 29 ++ tests/common.hpp | 17 +- tests/conn_check_health.cpp | 134 ++--- tests/conn_echo_stress.cpp | 17 +- tests/conn_exec.cpp | 61 ++- tests/conn_exec_cancel.cpp | 108 +--- tests/conn_exec_cancel2.cpp | 96 ++++ tests/conn_exec_error.cpp | 72 ++- tests/conn_exec_retry.cpp | 67 ++- tests/conn_push.cpp | 109 ++-- tests/conn_quit.cpp | 48 +- tests/conn_reconnect.cpp | 43 +- tests/conn_run_cancel.cpp | 51 +- tests/conn_tls.cpp | 51 +- tests/cpp20_low_level_async.cpp | 8 +- tests/issue_50.cpp | 58 ++- tests/run.cpp | 76 ++- 59 files changed, 2667 insertions(+), 2192 deletions(-) delete mode 100644 examples/cpp20_intro_awaitable_ops.cpp create mode 100644 examples/sync_connection.hpp delete mode 100644 include/boost/redis/address.hpp create mode 100644 include/boost/redis/config.hpp delete mode 100644 include/boost/redis/detail/connection_ops.hpp create mode 100644 include/boost/redis/detail/connector.hpp rename include/boost/redis/{check_health.hpp => detail/health_checker.hpp} (71%) create mode 100644 include/boost/redis/detail/reconnection.hpp create mode 100644 include/boost/redis/detail/resolver.hpp delete mode 100644 include/boost/redis/experimental/connector.hpp create mode 100644 include/boost/redis/impl/logger.ipp delete mode 100644 include/boost/redis/run.hpp create mode 100644 include/boost/redis/ssl/detail/handshaker.hpp create mode 100644 tests/common.cpp create mode 100644 tests/conn_exec_cancel2.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c30a93c..22a30ae5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,17 @@ include_directories(include) # Main function for the examples. #======================================================================= +add_library(test_common STATIC + tests/common.cpp +) +target_compile_features(test_common PUBLIC cxx_std_17) +if (MSVC) + target_compile_options(test_common PRIVATE /bigobj) + target_compile_definitions(test_common PRIVATE _WIN32_WINNT=0x0601) +endif() + +#======================================================================= + add_library(common STATIC examples/start.cpp examples/main.cpp @@ -81,15 +92,6 @@ if (MSVC) target_compile_definitions(cpp20_intro PRIVATE _WIN32_WINNT=0x0601) endif() -add_executable(cpp20_intro_awaitable_ops examples/cpp20_intro_awaitable_ops.cpp) -target_link_libraries(cpp20_intro_awaitable_ops common) -target_compile_features(cpp20_intro_awaitable_ops PUBLIC cxx_std_20) -add_test(cpp20_intro_awaitable_ops cpp20_intro_awaitable_ops) -if (MSVC) - target_compile_options(cpp20_intro_awaitable_ops PRIVATE /bigobj) - target_compile_definitions(cpp20_intro_awaitable_ops PRIVATE _WIN32_WINNT=0x0601) -endif() - add_executable(cpp17_intro examples/cpp17_intro.cpp) target_compile_features(cpp17_intro PUBLIC cxx_std_17) add_test(cpp17_intro cpp17_intro) @@ -156,10 +158,12 @@ if (Protobuf_FOUND) endif() endif() -if (NOT MSVC) add_executable(cpp20_subscriber examples/cpp20_subscriber.cpp) target_compile_features(cpp20_subscriber PUBLIC cxx_std_20) target_link_libraries(cpp20_subscriber common) +if (MSVC) + target_compile_options(cpp20_subscriber PRIVATE /bigobj) + target_compile_definitions(cpp20_subscriber PRIVATE _WIN32_WINNT=0x0601) endif() add_executable(cpp20_intro_tls examples/cpp20_intro_tls.cpp) @@ -205,6 +209,7 @@ endif() add_executable(test_conn_exec tests/conn_exec.cpp) target_compile_features(test_conn_exec PUBLIC cxx_std_20) +target_link_libraries(test_conn_exec test_common) add_test(test_conn_exec test_conn_exec) if (MSVC) target_compile_options(test_conn_exec PRIVATE /bigobj) @@ -213,6 +218,7 @@ endif() add_executable(test_conn_exec_retry tests/conn_exec_retry.cpp) target_compile_features(test_conn_exec_retry PUBLIC cxx_std_20) +target_link_libraries(test_conn_exec_retry test_common) add_test(test_conn_exec_retry test_conn_exec_retry) if (MSVC) target_compile_options(test_conn_exec_retry PRIVATE /bigobj) @@ -221,6 +227,7 @@ endif() add_executable(test_conn_push tests/conn_push.cpp) target_compile_features(test_conn_push PUBLIC cxx_std_20) +target_link_libraries(test_conn_push test_common) add_test(test_conn_push test_conn_push) if (MSVC) target_compile_options(test_conn_push PRIVATE /bigobj) @@ -237,7 +244,7 @@ endif() add_executable(test_conn_reconnect tests/conn_reconnect.cpp) target_compile_features(test_conn_reconnect PUBLIC cxx_std_20) -target_link_libraries(test_conn_reconnect common) +target_link_libraries(test_conn_reconnect common test_common) add_test(test_conn_reconnect test_conn_reconnect) if (MSVC) target_compile_options(test_conn_reconnect PRIVATE /bigobj) @@ -271,16 +278,25 @@ endif() add_executable(test_conn_exec_cancel tests/conn_exec_cancel.cpp) target_compile_features(test_conn_exec_cancel PUBLIC cxx_std_20) -target_link_libraries(test_conn_exec_cancel common) +target_link_libraries(test_conn_exec_cancel common test_common) add_test(test_conn_exec_cancel test_conn_exec_cancel) if (MSVC) target_compile_options(test_conn_exec_cancel PRIVATE /bigobj) target_compile_definitions(test_conn_exec_cancel PRIVATE _WIN32_WINNT=0x0601) endif() +add_executable(test_conn_exec_cancel2 tests/conn_exec_cancel2.cpp) +target_compile_features(test_conn_exec_cancel2 PUBLIC cxx_std_20) +target_link_libraries(test_conn_exec_cancel2 common test_common) +add_test(test_conn_exec_cancel2 test_conn_exec_cancel2) +if (MSVC) + target_compile_options(test_conn_exec_cancel2 PRIVATE /bigobj) + target_compile_definitions(test_conn_exec_cancel2 PRIVATE _WIN32_WINNT=0x0601) +endif() + add_executable(test_conn_exec_error tests/conn_exec_error.cpp) target_compile_features(test_conn_exec_error PUBLIC cxx_std_17) -target_link_libraries(test_conn_exec_error common) +target_link_libraries(test_conn_exec_error common test_common) add_test(test_conn_exec_error test_conn_exec_error) if (MSVC) target_compile_options(test_conn_exec_error PRIVATE /bigobj) @@ -289,7 +305,7 @@ endif() add_executable(test_conn_echo_stress tests/conn_echo_stress.cpp) target_compile_features(test_conn_echo_stress PUBLIC cxx_std_20) -target_link_libraries(test_conn_echo_stress common) +target_link_libraries(test_conn_echo_stress common test_common) add_test(test_conn_echo_stress test_conn_echo_stress) if (MSVC) target_compile_options(test_conn_echo_stress PRIVATE /bigobj) @@ -304,22 +320,27 @@ if (MSVC) target_compile_definitions(test_request PRIVATE _WIN32_WINNT=0x0601) endif() -if (NOT MSVC) add_executable(test_issue_50 tests/issue_50.cpp) target_compile_features(test_issue_50 PUBLIC cxx_std_20) target_link_libraries(test_issue_50 common) add_test(test_issue_50 test_issue_50) +if (MSVC) + target_compile_options(test_issue_50 PRIVATE /bigobj) + target_compile_definitions(test_issue_50 PRIVATE _WIN32_WINNT=0x0601) endif() -if (NOT MSVC) add_executable(test_conn_check_health tests/conn_check_health.cpp) target_compile_features(test_conn_check_health PUBLIC cxx_std_17) target_link_libraries(test_conn_check_health common) add_test(test_conn_check_health test_conn_check_health) +if (MSVC) + target_compile_options(test_conn_check_health PRIVATE /bigobj) + target_compile_definitions(test_conn_check_health PRIVATE _WIN32_WINNT=0x0601) endif() add_executable(test_run tests/run.cpp) target_compile_features(test_run PUBLIC cxx_std_17) +target_link_libraries(test_run test_common) add_test(test_run test_run) if (MSVC) target_compile_options(test_run PRIVATE /bigobj) diff --git a/README.md b/README.md index 0dfb7dc6..58ca1daa 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Boost.Redis is a [Redis](https://redis.io/) client library built on top of [Boost.Asio](https://www.boost.org/doc/libs/release/doc/html/boost_asio.html) -that implements -[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md), -a plain text protocol which can multiplex any number of client +that implements Redis plain text protocol +[RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md). +It can multiplex any number of client requests, responses, and server pushes onto a single active socket connection to the Redis server. The library hides low-level code away from the user, which, in the majority of the cases will be concerned @@ -17,7 +17,7 @@ with only three library entities STL containers and user defined data types. * `boost::redis::response`: Container of Redis responses. -In the next sections we will cover all those points in detail with +In the next sections we will cover all these points in detail with examples. The requirements for using Boost.Redis are * Boost 1.81 or greater. @@ -40,7 +40,7 @@ examples and tests cmake is supported, for example ```cpp # Linux -$ BOOST_ROOT=/opt/boost_1_81_0 cmake --preset dev +$ BOOST_ROOT=/opt/boost_1_81_0 cmake --preset g++-11 # Windows $ cmake -G "Visual Studio 17 2022" -A x64 -B bin64 -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake @@ -65,107 +65,34 @@ connection to send a [ping](https://redis.io/commands/ping/) command to Redis ```cpp -auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { - // From examples/common.hpp to avoid vebosity - co_await connect(conn, host, port); + auto conn = std::make_shared(co_await net::this_coro::executor); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - // async_run coordinates read and write operations. - co_await conn->async_run(); - - // Cancel pending operations, if any. - conn->cancel(operation::exec); - conn->cancel(operation::receive); -} - -auto co_main(std::string host, std::string port) -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, host, port), net::detached); - - // A request can contain multiple commands. + // A request containing only a ping command. request req; - req.push("HELLO", 3); req.push("PING", "Hello world"); - req.push("QUIT"); - // Stores responses of each individual command. The responses to - // HELLO and QUIT are being ignored for simplicity. - response resp; + // Response where the PONG response will be stored. + response resp; // Executes the request. co_await conn->async_exec(req, resp); + conn->cancel(); - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; } ``` + The roles played by the `async_run` and `async_exec` functions are * `connection::async_exec`: Execute the commands contained in the request and store the individual responses in the `resp` object. Can be called from multiple places in your code concurrently. -* `connection::async_run`: Coordinate low-level read and write - operations. More specifically, it will hand IO control to - `async_exec` when a response arrives and to `async_receive` when a - server-push is received. It is also responsible for triggering - writes of pending requests. - -Depending on the user's requirements, there are different styles of -calling `async_run`. For example, in a short-lived connection where -there is only one active client communicating with the server, the -easiest way to call `async_run` is to only run it simultaneously with -the `async_exec` call, this is exemplified in -cpp20_intro_awaitable_ops.cpp. If there are many in-process clients -performing simultaneous requests, an alternative is to launch a -long-running coroutine which calls `async_run` detached from other -operations as shown in the example above, cpp20_intro.cpp and -cpp20_echo_server.cpp. The list of examples below will help users -comparing different ways of implementing the ping example shown above - -* cpp20_intro_awaitable_ops.cpp: Uses awaitable operators. -* cpp20_intro.cpp: Calls `async_run` detached from other operations. -* cpp20_intro_tls.cpp: Communicates over TLS. -* cpp17_intro.cpp: Uses callbacks and requires C++17. -* cpp17_intro_sync.cpp: Runs `async_run` in a separate thread and - performs synchronous calls to `async_exec`. - -While calling `async_run` is a sufficient condition for maintaining -active two-way communication with the Redis server, most production -deployments will want to do more. For example, they may want to -reconnect if the connection goes down, either to the same server or a -failover server. They may want to perform health checks and more. The -example below shows for example how to use a loop to keep reconnecting -to the same address when a disconnection occurs (see -cpp20_subscriber.cpp) - -```cpp -auto run(std::shared_ptr conn) -> net::awaitable -{ - steady_timer timer{co_await net::this_coro::executor}; - - for (;;) { - co_await connect(conn, "127.0.0.1", "6379"); - co_await (conn->async_run() || health_check(conn) || receiver(conn)); - - // Prepare the stream for a new connection. - conn->reset_stream(); - - // Waits one second before trying to reconnect. - timer.expires_after(std::chrono::seconds{1}); - co_await timer.async_wait(); - } -} -``` - -The ability to reconnect the same connection object results in -considerable simplification of backend code and makes it easier to -write failover-safe applications. For example, a Websocket server -might have a 10k sessions communicating with Redis at the time the -connection is lost (or maybe killed by the server admin to force a -failover). It would be concerning if each individual section were to -throw exceptions and handle error. With the pattern shown above the -only place that has to manage the error is the run function. +* `connection::async_run`: Resolve, connect, ssl-handshake, + resp3-handshake, health-checks reconnection and coordinate low-level + read and write operations.. ### Server pushes @@ -181,49 +108,35 @@ The connection class supports server pushes by means of the to used it ```cpp -auto receiver(std::shared_ptr conn) -> net::awaitable +auto +receiver(std::shared_ptr conn) -> net::awaitable { - for (generic_response resp;;) { - co_await conn->async_receive(resp); - // Use resp and clear the response for a new push. - resp.clear(); + request req; + req.push("SUBSCRIBE", "channel"); + + while (!conn->is_cancelled()) { + + // Reconnect to channels. + co_await conn->async_exec(req); + + // Loop reading Redis pushs messages. + for (generic_response resp;;) { + error_code ec; + co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec)); + if (ec) + break; // Connection lost, break so we can reconnect to channels. + std::cout + << resp.value().at(1).value + << " " << resp.value().at(2).value + << " " << resp.value().at(3).value + << std::endl; + resp.value().clear(); + } } } + ``` -### Cancellation - -Boost.Redis supports both implicit and explicit cancellation of connection -operations. Explicit cancellation is supported by means of the -`boost::redis::connection::cancel` member function. Implicit -terminal-cancellation, like those that happen when using Asio -awaitable `operator ||` will be discussed with more detail below. - -```cpp -co_await (conn.async_run(...) || conn.async_exec(...)) -``` - -* Useful for short-lived connections that are meant to be closed after - a command has been executed. - -```cpp -co_await (conn.async_exec(...) || time.async_wait(...)) -``` - -* Provides a way to limit how long the execution of a single request - should last. -* WARNING: If the timer fires after the request has been sent but before the - response has been received, the connection will be closed. -* It is usually a better idea to have a healthy checker than adding - per request timeout, see cpp20_subscriber.cpp for an example. - -```cpp -co_await (conn.async_exec(...) || conn.async_exec(...) || ... || conn.async_exec(...)) -``` - -* This works but is unnecessary, the connection will automatically - merge the individual requests into a single payload. - ## Requests @@ -535,7 +448,6 @@ to serialize using json and [protobuf](https://protobuf.dev/). The examples below show how to use the features discussed so far -* cpp20_intro_awaitable_ops.cpp: The version shown above. * cpp20_intro.cpp: Does not use awaitable operators. * cpp20_intro_tls.cpp: Communicates over TLS. * cpp20_containers.cpp: Shows how to send and receive STL containers and how to use transactions. @@ -793,7 +705,7 @@ https://lists.boost.org/Archives/boost/2023/01/253944.php. * Renames the project to Boost.Redis and moves the code into namespace `boost::redis`. -* As pointed out in the reviews the `to_buld` and `from_buld` names were too +* As pointed out in the reviews the `to_bulk` and `from_bulk` names were too generic for ADL customization points. They gained the prefix `boost_redis_`. * Moves `boost::redis::resp3::request` to `boost::redis::request`. @@ -820,19 +732,16 @@ https://lists.boost.org/Archives/boost/2023/01/253944.php. became unnecessary and was removed. I could measure significative performance gains with theses changes. -* Adds native json support for Boost.Describe'd classes. To use it include - `` and decribe you class as of Boost.Describe, see - cpp20_json_serialization.cpp for more details. +* Improves serialization examples using Boost.Describe to serialize to JSON and protobuf. See + cpp20_json.cpp and cpp20_protobuf.cpp for more details. * Upgrades to Boost 1.81.0. * Fixes build with libc++. -* Adds a function that performs health checks, see - `boost::redis::experimental::async_check_health`. - -* Adds non-member `async_run` function that resolves, connects and - calls member `async_run` on a connection object. +* Adds high-level functionality to the connection classes. For + example, `boost::redis::connection::async_run` will automatically + resolve, connect, reconnect and perform health checks. ### v1.4.0-1 diff --git a/examples/cpp17_intro.cpp b/examples/cpp17_intro.cpp index f482d306..c6cbd6c8 100644 --- a/examples/cpp17_intro.cpp +++ b/examples/cpp17_intro.cpp @@ -4,59 +4,49 @@ * accompanying file LICENSE.txt) */ +#include +#include #include -#include -#include -#include + #include namespace net = boost::asio; using boost::redis::connection; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::logger; -using boost::redis::address; -using namespace std::chrono_literals; +using boost::redis::config; auto main(int argc, char * argv[]) -> int { try { - address addr; + config cfg; if (argc == 3) { - addr.host = argv[1]; - addr.port = argv[2]; + cfg.addr.host = argv[1]; + cfg.addr.port = argv[2]; } - // The request request req; - req.push("HELLO", 3); req.push("PING", "Hello world"); - // The response. - response resp; + response resp; net::io_context ioc; connection conn{ioc}; - async_run(conn, addr, 10s, 10s, logger{}, [&](auto){ - conn.cancel(); - }); + conn.async_run(cfg, {}, net::detached); - conn.async_exec(req, resp, [&](auto ec, auto){ + conn.async_exec(req, resp, [&](auto ec, auto) { if (!ec) - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; conn.cancel(); }); ioc.run(); - return 0; } catch (std::exception const& e) { std::cerr << "Error: " << e.what() << std::endl; + return 1; } - return 1; } diff --git a/examples/cpp17_intro_sync.cpp b/examples/cpp17_intro_sync.cpp index 20aff7ef..1969c3ce 100644 --- a/examples/cpp17_intro_sync.cpp +++ b/examples/cpp17_intro_sync.cpp @@ -4,84 +4,42 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include -#include -#include -#include -#include -#include +#include "sync_connection.hpp" + #include -#include -#include #include // Include this in no more than one .cpp file. #include -namespace net = boost::asio; -using connection = boost::redis::connection; -using boost::redis::operation; +using boost::redis::sync_connection; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using boost::redis::logger; -using boost::redis::async_check_health; -using namespace std::chrono_literals; - -template -auto exec(std::shared_ptr conn, request const& req, Response& resp) -{ - net::dispatch( - conn->get_executor(), - net::deferred([&]() { return conn->async_exec(req, resp, net::deferred); })) - (net::use_future).get(); -} +using boost::redis::config; auto main(int argc, char * argv[]) -> int { try { - address addr; + config cfg; if (argc == 3) { - addr.host = argv[1]; - addr.port = argv[2]; + cfg.addr.host = argv[1]; + cfg.addr.port = argv[2]; } - net::io_context ioc{1}; - - auto conn = std::make_shared(ioc); - - // Starts a thread that will can io_context::run on which the - // connection will run. - std::thread t{[&ioc, conn, addr]() { - async_run(*conn, addr, 10s, 10s, logger{}, [conn](auto){ - conn->cancel(); - }); - - async_check_health(*conn, "Boost.Redis", 2s, [conn](auto) { - conn->cancel(); - }); - - ioc.run(); - }}; + sync_connection conn; + conn.run(cfg); request req; - req.push("HELLO", 3); req.push("PING"); - req.push("QUIT"); - response resp; + response resp; - // Executes commands synchronously. - exec(conn, req, resp); + conn.exec(req, resp); + conn.stop(); - std::cout << "Response: " << std::get<1>(resp).value() << std::endl; + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; - t.join(); } catch (std::exception const& e) { std::cerr << e.what() << std::endl; } diff --git a/examples/cpp20_chat_room.cpp b/examples/cpp20_chat_room.cpp index c8f77f8e..57df7901 100644 --- a/examples/cpp20_chat_room.cpp +++ b/examples/cpp20_chat_room.cpp @@ -4,39 +4,58 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include +#include +#include #include +#include +#include +#include #include #include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include #if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using stream_descriptor = net::use_awaitable_t<>::as_default_on_t; -using signal_set = net::use_awaitable_t<>::as_default_on_t; +using stream_descriptor = net::deferred_t::as_default_on_t; +using connection = net::deferred_t::as_default_on_t; +using signal_set = net::deferred_t::as_default_on_t; using boost::redis::request; using boost::redis::generic_response; -using boost::redis::async_check_health; -using boost::redis::async_run; -using boost::redis::address; -using connection = net::use_awaitable_t<>::as_default_on_t; +using boost::redis::config; +using net::redirect_error; +using net::use_awaitable; +using boost::system::error_code; +using namespace std::chrono_literals; // Chat over Redis pubsub. To test, run this program from multiple // terminals and type messages to stdin. -// Receives Redis pushes. -auto receiver(std::shared_ptr conn) -> net::awaitable +auto +receiver(std::shared_ptr conn) -> net::awaitable { - for (generic_response resp;;) { - co_await conn->async_receive(resp); - std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " " << resp.value().at(3).value << std::endl; - resp.value().clear(); + request req; + req.push("SUBSCRIBE", "channel"); + + while (!conn->is_cancelled()) { + + // Subscribe to channels. + co_await conn->async_exec(req); + + // Loop reading Redis push messages. + for (generic_response resp;;) { + error_code ec; + co_await conn->async_receive(resp, redirect_error(use_awaitable, ec)); + if (ec) + break; // Connection lost, break so we can reconnect to channels. + std::cout + << resp.value().at(1).value + << " " << resp.value().at(2).value + << " " << resp.value().at(3).value + << std::endl; + resp.value().clear(); + } } } @@ -46,31 +65,31 @@ auto publisher(std::shared_ptr in, std::shared_ptrasync_exec(req); msg.erase(0, n); } } // Called from the main function (see main.cpp) -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); auto stream = std::make_shared(ex, ::dup(STDIN_FILENO)); - signal_set sig{ex, SIGINT, SIGTERM}; - request req; - req.push("HELLO", 3); - req.push("SUBSCRIBE", "chat-channel"); + net::co_spawn(ex, receiver(conn), net::detached); + net::co_spawn(ex, publisher(stream, conn), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - co_await ((async_run(*conn, addr) || publisher(stream, conn) || receiver(conn) || - async_check_health(*conn) || sig.async_wait()) && - conn->async_exec(req)); + signal_set sig_set{ex, SIGINT, SIGTERM}; + co_await sig_set.async_wait(); + conn->cancel(); + stream->cancel(); } #else // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR) -auto co_main(address const&) -> net::awaitable +auto co_main(config const&) -> net::awaitable { std::cout << "Requires support for posix streams." << std::endl; co_return; diff --git a/examples/cpp20_containers.cpp b/examples/cpp20_containers.cpp index 63785a50..277a8575 100644 --- a/examples/cpp20_containers.cpp +++ b/examples/cpp20_containers.cpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include #include #include #include @@ -16,12 +16,12 @@ namespace net = boost::asio; namespace redis = boost::redis; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; +using redis::request; +using redis::response; +using redis::ignore_t; +using redis::ignore; +using redis::config; +using connection = net::deferred_t::as_default_on_t; void print(std::map const& cont) { @@ -35,11 +35,6 @@ void print(std::vector const& cont) std::cout << "\n"; } -auto run(std::shared_ptr conn, address const& addr) -> net::awaitable -{ - co_await async_run(*conn, addr); -} - // Stores the content of some STL containers in Redis. auto store(std::shared_ptr conn) -> net::awaitable { @@ -50,7 +45,6 @@ auto store(std::shared_ptr conn) -> net::awaitable {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}; request req; - req.push("HELLO", 3); req.push_range("RPUSH", "rpush-key", vec); req.push_range("HSET", "hset-key", map); @@ -61,30 +55,27 @@ auto hgetall(std::shared_ptr conn) -> net::awaitable { // A request contains multiple commands. request req; - req.push("HELLO", 3); req.push("HGETALL", "hset-key"); // Responses as tuple elements. - response> resp; + response> resp; // Executes the request and reads the response. co_await conn->async_exec(req, resp); - print(std::get<1>(resp).value()); + print(std::get<0>(resp).value()); } // Retrieves in a transaction. auto transaction(std::shared_ptr conn) -> net::awaitable { request req; - req.push("HELLO", 3); req.push("MULTI"); req.push("LRANGE", "rpush-key", 0, -1); // Retrieves req.push("HGETALL", "hset-key"); // Retrieves req.push("EXEC"); response< - ignore_t, // hello ignore_t, // multi ignore_t, // lrange ignore_t, // hgetall @@ -93,28 +84,20 @@ auto transaction(std::shared_ptr conn) -> net::awaitable co_await conn->async_exec(req, resp); - print(std::get<0>(std::get<4>(resp).value()).value().value()); - print(std::get<1>(std::get<4>(resp).value()).value().value()); -} - -auto quit(std::shared_ptr conn) -> net::awaitable -{ - request req; - req.push("QUIT"); - - co_await conn->async_exec(req); + print(std::get<0>(std::get<3>(resp).value()).value().value()); + print(std::get<1>(std::get<3>(resp).value()).value().value()); } // Called from the main function (see main.cpp) -net::awaitable co_main(address const& addr) +net::awaitable co_main(config const& cfg) { - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, addr), net::detached); + auto conn = std::make_shared(co_await net::this_coro::executor); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); + co_await store(conn); co_await transaction(conn); co_await hgetall(conn); - co_await quit(conn); + conn->cancel(); } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_echo_server.cpp b/examples/cpp20_echo_server.cpp index 8d5db7af..44b7b657 100644 --- a/examples/cpp20_echo_server.cpp +++ b/examples/cpp20_echo_server.cpp @@ -4,26 +4,26 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include +#include +#include #include #include +#include +#include +#include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using tcp_socket = net::use_awaitable_t<>::as_default_on_t; -using tcp_acceptor = net::use_awaitable_t<>::as_default_on_t; -using signal_set = net::use_awaitable_t<>::as_default_on_t; +using tcp_socket = net::deferred_t::as_default_on_t; +using tcp_acceptor = net::deferred_t::as_default_on_t; +using signal_set = net::deferred_t::as_default_on_t; +using connection = net::deferred_t::as_default_on_t; using boost::redis::request; using boost::redis::response; -using boost::redis::async_check_health; -using boost::redis::async_run; -using boost::redis::address; -using connection = net::use_awaitable_t<>::as_default_on_t; +using boost::redis::config; +using boost::system::error_code; +using namespace std::chrono_literals; auto echo_server_session(tcp_socket socket, std::shared_ptr conn) -> net::awaitable { @@ -44,24 +44,27 @@ auto echo_server_session(tcp_socket socket, std::shared_ptr conn) -> // Listens for tcp connections. auto listener(std::shared_ptr conn) -> net::awaitable { - auto ex = co_await net::this_coro::executor; - tcp_acceptor acc(ex, {net::ip::tcp::v4(), 55555}); - for (;;) - net::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), net::detached); + try { + auto ex = co_await net::this_coro::executor; + tcp_acceptor acc(ex, {net::ip::tcp::v4(), 55555}); + for (;;) + net::co_spawn(ex, echo_server_session(co_await acc.async_accept(), conn), net::detached); + } catch (std::exception const& e) { + std::clog << "Listener: " << e.what() << std::endl; + } } // Called from the main function (see main.cpp) -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - signal_set sig{ex, SIGINT, SIGTERM}; + net::co_spawn(ex, listener(conn), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - request req; - req.push("HELLO", 3); - - co_await ((async_run(*conn, addr) || listener(conn) || async_check_health(*conn) || - sig.async_wait()) && conn->async_exec(req)); + signal_set sig_set(ex, SIGINT, SIGTERM); + co_await sig_set.async_wait(); + conn->cancel(); } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_intro.cpp b/examples/cpp20_intro.cpp index 79ae4b6e..a67db286 100644 --- a/examples/cpp20_intro.cpp +++ b/examples/cpp20_intro.cpp @@ -4,10 +4,11 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include #include #include +#include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) @@ -15,41 +16,28 @@ namespace net = boost::asio; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using connection = net::use_awaitable_t<>::as_default_on_t; - -auto run(std::shared_ptr conn, address addr) -> net::awaitable -{ - // async_run coordinate read and write operations. - co_await async_run(*conn, addr); - - // Cancel pending operations, if any. - conn->cancel(); -} +using boost::redis::config; +using boost::redis::logger; +using connection = net::deferred_t::as_default_on_t; // Called from the main function (see main.cpp) -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { - auto ex = co_await net::this_coro::executor; - auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, addr), net::detached); + auto conn = std::make_shared(co_await net::this_coro::executor); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - // A request can contain multiple commands. + // A request containing only a ping command. request req; - req.push("HELLO", 3); req.push("PING", "Hello world"); - req.push("QUIT"); - // Stores responses of each individual command. The responses to - // HELLO and QUIT are being ignored for simplicity. - response resp; + // Response where the PONG response will be stored. + response resp; - // Executtes the request. + // Executes the request. co_await conn->async_exec(req, resp); + conn->cancel(); - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; + std::cout << "PING: " << std::get<0>(resp).value() << std::endl; } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_intro_awaitable_ops.cpp b/examples/cpp20_intro_awaitable_ops.cpp deleted file mode 100644 index a58b8552..00000000 --- a/examples/cpp20_intro_awaitable_ops.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* 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 - -#if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include - -namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; - -// Called from the main function (see main.cpp) -auto co_main(address const& addr) -> net::awaitable -{ - try { - request req; - req.push("HELLO", 3); - req.push("PING", "Hello world"); - req.push("QUIT"); - - response resp; - - connection conn{co_await net::this_coro::executor}; - co_await (async_run(conn, addr) || conn.async_exec(req, resp)); - - std::cout << "PING: " << std::get<1>(resp).value() << std::endl; - } catch (std::exception const& e) { - std::cout << e.what() << std::endl; - } -} - -#endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_intro_tls.cpp b/examples/cpp20_intro_tls.cpp index 2acc07ed..00c1c9a4 100644 --- a/examples/cpp20_intro_tls.cpp +++ b/examples/cpp20_intro_tls.cpp @@ -5,27 +5,20 @@ */ #include -#include -#include +#include #include -#include -#include -#include -#include +#include +#include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include namespace net = boost::asio; -namespace redis = boost::redis; -using namespace net::experimental::awaitable_operators; -using resolver = net::use_awaitable_t<>::as_default_on_t; -using connection = net::use_awaitable_t<>::as_default_on_t; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::address; +using boost::redis::config; +using boost::redis::logger; +using connection = net::deferred_t::as_default_on_t; auto verify_certificate(bool, net::ssl::verify_context&) -> bool { @@ -33,30 +26,30 @@ auto verify_certificate(bool, net::ssl::verify_context&) -> bool return true; } -net::awaitable co_main(address const&) +auto co_main(config const&) -> net::awaitable { - request req; - req.push("HELLO", 3, "AUTH", "aedis", "aedis"); - req.push("PING"); - req.push("QUIT"); - - response resp; - - // Resolve - auto ex = co_await net::this_coro::executor; - resolver resv{ex}; - auto const endpoints = co_await resv.async_resolve("db.occase.de", "6380"); + config cfg; + cfg.username = "aedis"; + cfg.password = "aedis"; + cfg.addr.host = "db.occase.de"; + cfg.addr.port = "6380"; net::ssl::context ctx{net::ssl::context::sslv23}; - connection conn{ex, ctx}; - conn.next_layer().set_verify_mode(net::ssl::verify_peer); - conn.next_layer().set_verify_callback(verify_certificate); + auto conn = std::make_shared(co_await net::this_coro::executor, ctx); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); - co_await net::async_connect(conn.lowest_layer(), endpoints); - co_await conn.next_layer().async_handshake(net::ssl::stream_base::client); - co_await (conn.async_run() || conn.async_exec(req, resp)); + request req; + req.push("PING"); - std::cout << "Response: " << std::get<1>(resp).value() << std::endl; + response resp; + + conn->next_layer().set_verify_mode(net::ssl::verify_peer); + conn->next_layer().set_verify_callback(verify_certificate); + + co_await conn->async_exec(req, resp); + conn->cancel(); + + std::cout << "Response: " << std::get<0>(resp).value() << std::endl; } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_json.cpp b/examples/cpp20_json.cpp index 0427b03a..5b204b2f 100644 --- a/examples/cpp20_json.cpp +++ b/examples/cpp20_json.cpp @@ -4,31 +4,29 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include #include #include #include +#include #include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -// Include this in no more than one .cpp file. #define BOOST_JSON_NO_LIB #define BOOST_CONTAINER_NO_LIB #include "json.hpp" #include namespace net = boost::asio; -namespace redis = boost::redis; using namespace boost::describe; +using connection = net::deferred_t::as_default_on_t; using boost::redis::request; using boost::redis::response; using boost::redis::operation; using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; +using boost::redis::config; // Struct that will be stored in Redis using json serialization. struct user { @@ -47,37 +45,30 @@ void boost_redis_to_bulk(std::string& to, user const& u) void boost_redis_from_bulk(user& u, std::string_view sv, boost::system::error_code& ec) { boost::redis::json::from_bulk(u, sv, ec); } -auto run(std::shared_ptr conn, address addr) -> net::awaitable -{ - co_await async_run(*conn, addr); -} - -net::awaitable co_main(address const& addr) +net::awaitable co_main(config const& cfg) { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, addr), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); // user object that will be stored in Redis in json format. user const u{"Joao", "58", "Brazil"}; // Stores and retrieves in the same request. request req; - req.push("HELLO", 3); req.push("SET", "json-key", u); // Stores in Redis. req.push("GET", "json-key"); // Retrieves from Redis. - response resp; + response resp; co_await conn->async_exec(req, resp); + conn->cancel(); // Prints the first ping std::cout - << "Name: " << std::get<2>(resp).value().name << "\n" - << "Age: " << std::get<2>(resp).value().age << "\n" - << "Country: " << std::get<2>(resp).value().country << "\n"; - - conn->cancel(operation::run); + << "Name: " << std::get<1>(resp).value().name << "\n" + << "Age: " << std::get<1>(resp).value().age << "\n" + << "Country: " << std::get<1>(resp).value().country << "\n"; } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_protobuf.cpp b/examples/cpp20_protobuf.cpp index a3e9cbc3..200bfe1d 100644 --- a/examples/cpp20_protobuf.cpp +++ b/examples/cpp20_protobuf.cpp @@ -4,10 +4,11 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include +#include #include #include +#include #include #include "protobuf.hpp" @@ -18,14 +19,12 @@ #if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; -namespace redis = boost::redis; using boost::redis::request; using boost::redis::response; using boost::redis::operation; using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using connection = net::use_awaitable_t<>::as_default_on_t; +using boost::redis::config; +using connection = net::deferred_t::as_default_on_t; // The protobuf type described in examples/person.proto using tutorial::person; @@ -45,16 +44,11 @@ void boost_redis_from_bulk(person& u, std::string_view sv, boost::system::error_ using tutorial::boost_redis_to_bulk; using tutorial::boost_redis_from_bulk; -auto run(std::shared_ptr conn, address const& addr) -> net::awaitable -{ - co_await async_run(*conn, addr); -} - -net::awaitable co_main(address const& addr) +net::awaitable co_main(config const& cfg) { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - net::co_spawn(ex, run(conn, addr), net::detached); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); person p; p.set_name("Louis"); @@ -62,21 +56,19 @@ net::awaitable co_main(address const& addr) p.set_email("No email yet."); request req; - req.push("HELLO", 3); req.push("SET", "protobuf-key", p); req.push("GET", "protobuf-key"); - response resp; + response resp; // Sends the request and receives the response. co_await conn->async_exec(req, resp); + conn->cancel(); std::cout - << "Name: " << std::get<2>(resp).value().name() << "\n" - << "Age: " << std::get<2>(resp).value().id() << "\n" - << "Email: " << std::get<2>(resp).value().email() << "\n"; - - conn->cancel(operation::run); + << "Name: " << std::get<1>(resp).value().name() << "\n" + << "Age: " << std::get<1>(resp).value().id() << "\n" + << "Email: " << std::get<1>(resp).value().email() << "\n"; } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/cpp20_resolve_with_sentinel.cpp b/examples/cpp20_resolve_with_sentinel.cpp index aeb33c2f..1dbb01f3 100644 --- a/examples/cpp20_resolve_with_sentinel.cpp +++ b/examples/cpp20_resolve_with_sentinel.cpp @@ -4,22 +4,22 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include +#include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -#include namespace net = boost::asio; -using namespace net::experimental::awaitable_operators; +namespace redis = boost::redis; using endpoints = net::ip::tcp::resolver::results_type; -using boost::redis::request; -using boost::redis::response; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; +using redis::request; +using redis::response; +using redis::ignore_t; +using redis::config; +using redis::address; using connection = boost::asio::use_awaitable_t<>::as_default_on_t; auto redir(boost::system::error_code& ec) @@ -39,23 +39,30 @@ auto resolve_master_address(std::vector
const& addresses) -> net::await response>, ignore_t> resp; for (auto addr : addresses) { boost::system::error_code ec; - co_await (async_run(*conn, addr) && conn->async_exec(req, resp, redir(ec))); + config cfg; + cfg.addr = addr; + // TODO: async_run and async_exec should be lauched in + // parallel here so we can wait for async_run completion + // before eventually calling it again. + conn->async_run(cfg, {}, net::consign(net::detached, conn)); + co_await conn->async_exec(req, resp, redir(ec)); + conn->cancel(); conn->reset_stream(); - if (std::get<0>(resp)) + if (!ec && std::get<0>(resp)) co_return address{std::get<0>(resp).value().value().at(0), std::get<0>(resp).value().value().at(1)}; } co_return address{}; } -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { // A list of sentinel addresses from which only one is responsive. // This simulates sentinels that are down. std::vector
const addresses { address{"foo", "26379"} , address{"bar", "26379"} - , addr + , cfg.addr }; auto const ep = co_await resolve_master_address(addresses); diff --git a/examples/cpp20_subscriber.cpp b/examples/cpp20_subscriber.cpp index 6085af42..717b46fb 100644 --- a/examples/cpp20_subscriber.cpp +++ b/examples/cpp20_subscriber.cpp @@ -4,25 +4,29 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include #include +#include #include #include #include +#include +#include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; -namespace redis = boost::redis; -using redis::generic_response; -using redis::address; -using redis::logger; -using redis::experimental::async_connect; -using redis::experimental::connect_config; -using connection = net::use_awaitable_t<>::as_default_on_t; +using boost::redis::request; +using boost::redis::generic_response; +using boost::redis::logger; +using boost::redis::config; +using boost::system::error_code; +using connection = net::deferred_t::as_default_on_t; +using signal_set = net::deferred_t::as_default_on_t; +using namespace std::chrono_literals; /* This example will subscribe and read pushes indefinitely. * @@ -41,27 +45,44 @@ using connection = net::use_awaitable_t<>::as_default_on_t; */ // Receives server pushes. -auto receiver(std::shared_ptr conn) -> net::awaitable +auto +receiver(std::shared_ptr conn) -> net::awaitable { - for (generic_response resp;;) { - co_await conn->async_receive(resp); - std::cout - << resp.value().at(1).value - << " " << resp.value().at(2).value - << " " << resp.value().at(3).value - << std::endl; - resp.value().clear(); + request req; + req.push("SUBSCRIBE", "channel"); + + while (!conn->is_cancelled()) { + + // Reconnect to channels. + co_await conn->async_exec(req); + + // Loop reading Redis pushs messages. + for (generic_response resp;;) { + error_code ec; + co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec)); + if (ec) + break; // Connection lost, break so we can reconnect to channels. + std::cout + << resp.value().at(1).value + << " " << resp.value().at(2).value + << " " << resp.value().at(3).value + << std::endl; + resp.value().clear(); + } } } -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - connect_config cfg; - cfg.addr = addr; net::co_spawn(ex, receiver(conn), net::detached); - redis::experimental::async_connect(*conn, cfg, logger{}, net::consign(net::detached, conn)); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); + + signal_set sig_set(ex, SIGINT, SIGTERM); + co_await sig_set.async_wait(); + + conn->cancel(); } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/main.cpp b/examples/main.cpp index ff0b527d..b35146ac 100644 --- a/examples/main.cpp +++ b/examples/main.cpp @@ -5,25 +5,27 @@ */ #include "start.hpp" -#include +#include #include #include #include #if defined(BOOST_ASIO_HAS_CO_AWAIT) -extern boost::asio::awaitable co_main(boost::redis::address const&); +using boost::redis::config; + +extern boost::asio::awaitable co_main(config const&); auto main(int argc, char * argv[]) -> int { - boost::redis::address addr; + config cfg; if (argc == 3) { - addr.host = argv[1]; - addr.port = argv[2]; + cfg.addr.host = argv[1]; + cfg.addr.port = argv[2]; } - return start(co_main(addr)); + return start(co_main(cfg)); } #else // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/examples/start.cpp b/examples/start.cpp index eb8674ac..fb6a9f01 100644 --- a/examples/start.cpp +++ b/examples/start.cpp @@ -27,7 +27,7 @@ auto start(net::awaitable op) -> int return 0; } catch (std::exception const& e) { - std::cerr << "Error: " << e.what() << std::endl; + std::cerr << "start> " << e.what() << std::endl; } return 1; diff --git a/examples/sync_connection.hpp b/examples/sync_connection.hpp new file mode 100644 index 00000000..cc982f84 --- /dev/null +++ b/examples/sync_connection.hpp @@ -0,0 +1,63 @@ + +/* 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 +#include +#include +#include + +using namespace std::chrono_literals; + +namespace boost::redis +{ + +class sync_connection { +public: + sync_connection() + : ioc_{1} + , conn_{std::make_shared(ioc_)} + { } + + ~sync_connection() + { + thread_.join(); + } + + void run(config cfg) + { + // Starts a thread that will can io_context::run on which the + // connection will run. + thread_ = std::thread{[this, cfg]() { + conn_->async_run(cfg, {}, asio::detached); + ioc_.run(); + }}; + } + + void stop() + { + asio::dispatch(ioc_, [this]() { conn_->cancel(); }); + } + + template + auto exec(request const& req, Response& resp) + { + asio::dispatch( + conn_->get_executor(), + asio::deferred([this, &req, &resp]() { return conn_->async_exec(req, resp, asio::deferred); })) + (asio::use_future).get(); + } + +private: + asio::io_context ioc_{1}; + std::shared_ptr conn_; + std::thread thread_; +}; + +} diff --git a/include/boost/redis.hpp b/include/boost/redis.hpp index cd64c3fb..15515c9f 100644 --- a/include/boost/redis.hpp +++ b/include/boost/redis.hpp @@ -7,12 +7,15 @@ #ifndef BOOST_REDIS_HPP #define BOOST_REDIS_HPP +#include #include #include #include #include #include #include +#include +#include /** @defgroup high-level-api Reference * diff --git a/include/boost/redis/adapter/detail/adapters.hpp b/include/boost/redis/adapter/detail/adapters.hpp index 46705bb2..43bf6866 100644 --- a/include/boost/redis/adapter/detail/adapters.hpp +++ b/include/boost/redis/adapter/detail/adapters.hpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include diff --git a/include/boost/redis/adapter/detail/result_traits.hpp b/include/boost/redis/adapter/detail/result_traits.hpp index 2cc6afc9..09c3b520 100644 --- a/include/boost/redis/adapter/detail/result_traits.hpp +++ b/include/boost/redis/adapter/detail/result_traits.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/include/boost/redis/address.hpp b/include/boost/redis/address.hpp deleted file mode 100644 index a4af57d8..00000000 --- a/include/boost/redis/address.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_ADDRESS_HPP -#define BOOST_REDIS_ADDRESS_HPP - -#include - -namespace boost::redis -{ - -/** @brief Address of a Redis server - * @ingroup high-level-api - */ -struct address { - /// Redis host. - std::string host = "127.0.0.1"; - /// Redis port. - std::string port = "6379"; -}; - -} // boost::redis - -#endif // BOOST_REDIS_ADDRESS_HPP diff --git a/include/boost/redis/config.hpp b/include/boost/redis/config.hpp new file mode 100644 index 00000000..0abc4016 --- /dev/null +++ b/include/boost/redis/config.hpp @@ -0,0 +1,72 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_CONFIG_HPP +#define BOOST_REDIS_CONFIG_HPP + +#include +#include + +namespace boost::redis +{ + +/** @brief Address of a Redis server + * @ingroup high-level-api + */ +struct address { + /// Redis host. + std::string host = "127.0.0.1"; + /// Redis port. + std::string port = "6379"; +}; + +/** @brief Configure parameters used by the connection classes + * @ingroup high-level-api + */ +struct config { + /// Address of the Redis server. + address addr = address{"127.0.0.1", "6379"}; + + /** @brief Username passed to the + * [HELLO](https://redis.io/commands/hello/) command. If left + * empty `HELLO` will be sent without authentication parameters. + */ + std::string username; + + /** @brief Password passed to the + * [HELLO](https://redis.io/commands/hello/) command. If left + * empty `HELLO` will be sent without authentication parameters. + */ + std::string password; + + /// Client name parameter of the [HELLO](https://redis.io/commands/hello/) command. + std::string clientname = "Boost.Redis"; + + /// Message used by the health-checker in `boost::redis::connection::async_run`. + std::string health_check_id = "Boost.Redis"; + + /// Logger prefix, see `boost::redis::logger`. + std::string log_prefix = "(Boost.Redis) "; + + /// Time the resolve operation is allowed to last. + std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10}; + + /// Time the connect operation is allowed to last. + std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10}; + + /// Time the SSL handshake operation is allowed to last. + std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10}; + + /// @brief Health checks interval. + std::chrono::steady_clock::duration health_check_interval = std::chrono::seconds{2}; + + /// Time waited before trying a reconnection. + std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1}; +}; + +} // boost::redis + +#endif // BOOST_REDIS_CONFIG_HPP diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index abdaa9d4..931e4fbb 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -8,6 +8,10 @@ #define BOOST_REDIS_CONNECTION_HPP #include +#include +#include +#include +#include #include #include @@ -16,6 +20,27 @@ namespace boost::redis { +namespace detail +{ + +template +class dummy_handshaker { +public: + dummy_handshaker(Executor) {} + + template + auto async_handshake(Stream&, CompletionToken&& token) + { return asio::post(std::move(token)); } + + void set_config(config const&) {} + + std::size_t cancel(operation) { return 0;} + + constexpr bool is_dummy() const noexcept {return true;} +}; + +} + /** @brief A connection to the Redis server. * @ingroup high-level-api * @@ -50,8 +75,9 @@ public: explicit basic_connection(executor_type ex) : base_type{ex} + , reconn_{ex} + , runner_{ex, {}} , stream_{ex} - , reconnect_{true} {} /// Contructs from a context. @@ -67,9 +93,9 @@ public: void reset_stream() { if (stream_.is_open()) { - system::error_code ignore; - stream_.shutdown(asio::ip::tcp::socket::shutdown_both, ignore); - stream_.close(ignore); + system::error_code ec; + stream_.shutdown(asio::ip::tcp::socket::shutdown_both, ec); + stream_.close(ec); } } @@ -79,13 +105,31 @@ public: /// Returns a const reference to the next layer. auto next_layer() const noexcept -> auto const& { return stream_; } - /** @brief Starts read and write operations + /** @brief Starts underlying connection operations. * - * This function starts read and write operations with the Redis + * In more detail, this function will + * + * 1. Resolve the address passed on `boost::redis::config::addr`. + * 2. Connect to one of the results obtained in the resolve operation. + * 3. Send a [HELLO](https://redis.io/commands/hello/) command where each of its parameters are read from `cfg`. + * 4. Start a health-check operation where ping commands are sent + * at intervals specified in + * `boost::redis::config::health_check_interval`. The message passed to + * `PING` will be `boost::redis::config::health_check_id`. Passing a + * timeout with value zero will disable health-checks. If the Redis + * server does not respond to a health-check within two times the value + * specified here, it will be considered unresponsive and the connection + * will be closed and a new connection will be stablished. + * 5. Starts read and write operations with the Redis * server. More specifically it will trigger the write of all * requests i.e. calls to `async_exec` that happened prior to this * call. * + * When a connection is lost for any reason, a new one is stablished automatically. To disable + * reconnection call `boost::redis::connection::cancel(operation::reconnection)`. + * + * @param cfg Configuration paramters. + * @param l Logger object. The interface expected is specified in the class `boost::redis::logger`. * @param token Completion token. * * The completion token must have the following signature @@ -96,51 +140,37 @@ public: * * @remarks * - * * This function will complete only when the connection is lost. - * If the error is asio::error::eof this function will complete - * without error. - * * It can can be called multiple times on the same connection - * object. This makes it simple to implement reconnection in a way - * that does not require cancelling any pending connections. + * * This function will complete only if reconnection was disabled and the connection is lost. * - * For examples of how to call this function see the examples. For - * example, if reconnection is not necessary, the coroutine below - * is enough - * - * ```cpp - * auto run(std::shared_ptr conn, std::string host, std::string port) -> net::awaitable - * { - * // From examples/common.hpp to avoid vebosity - * co_await connect(conn, host, port); - * - * // async_run coordinate read and write operations. - * co_await conn->async_run(); - * - * // Cancel pending operations, if any. - * conn->cancel(operation::exec); - * conn->cancel(operation::receive); - * } - * ``` - * - * For a reconnection example see cpp20_subscriber.cpp. + * For example on how to call this function refer to cpp20_intro.cpp or any other example. */ - template > - auto async_run(CompletionToken token = CompletionToken{}) + template < + class Logger = logger, + class CompletionToken = asio::default_completion_token_t> + auto + async_run( + config const& cfg = {}, + Logger l = Logger{}, + CompletionToken token = CompletionToken{}) { - return base_type::async_run(std::move(token)); + reconn_.set_wait_interval(cfg.reconnect_wait_interval); + runner_.set_config(cfg); + l.set_prefix(runner_.get_config().log_prefix); + return reconn_.async_run(*this, l, std::move(token)); } - /** @brief Executes a command on the Redis server asynchronously. + /** @brief Executes commands on the Redis server asynchronously. * * This function sends a request to the Redis server and - * complete after the response has been processed. If the request + * waits for the responses to each individual command in the + * request to arrive. If the request * contains only commands that don't expect a response, the * completion occurs after it has been written to the underlying * stream. Multiple concurrent calls to this function will be * automatically queued by the implementation. * * @param req Request object. - * @param response Response object. + * @param resp Response object. * @param token Asio completion token. * * For an example see cpp20_echo_server.cpp. The completion token must @@ -158,17 +188,17 @@ public: class CompletionToken = asio::default_completion_token_t> auto async_exec( request const& req, - Response& response = ignore, + Response& resp = ignore, CompletionToken token = CompletionToken{}) { - return base_type::async_exec(req, response, std::move(token)); + return base_type::async_exec(req, resp, std::move(token)); } /** @brief Receives server side pushes asynchronously. * - * When pushes arrive and there is no async_receive operation in + * When pushes arrive and there is no `async_receive` operation in * progress, pushed data, requests, and responses will be paused - * until async_receive is called again. Apps will usually want to + * until `async_receive` is called again. Apps will usually want to * call `async_receive` in a loop. * * To cancel an ongoing receive operation apps should call @@ -203,15 +233,18 @@ public: * `async_exec`. Affects only requests that haven't been written * yet. * @li operation::run: Cancels the `async_run` operation. - * @li operation::receive: Cancels any ongoing calls to * `async_receive`. - * @li operation::all: Cancels all operations listed above. This - * is the default argument. + * @li operation::receive: Cancels any ongoing calls to `async_receive`. + * @li operation::all: Cancels all operations listed above. * * @param op: The operation to be cancelled. * @returns The number of operations that have been canceled. */ auto cancel(operation op = operation::all) -> std::size_t - { return base_type::cancel(op); } + { + reconn_.cancel(op); + runner_.cancel(op); + return base_type::cancel(op); + } /// Sets the maximum size of the read buffer. void set_max_buffer_read_size(std::size_t max_read_size) noexcept @@ -228,40 +261,43 @@ public: void reserve(std::size_t read, std::size_t write) { base_type::reserve(read, write); } - /** @brief Enable reconnection - * - * This property plays any role only when used with - * `boost::redis::async_run`. - */ - void enable_reconnection() noexcept {reconnect_ = true;} - - /** @brief Disable reconnection - * - * This property plays any role only when used with - * `boost::redis::async_run`. - */ - void disable_reconnection() noexcept {reconnect_ = false;} - - bool reconnect() const noexcept {return reconnect_;} + /// Returns true if the connection was canceled. + bool is_cancelled() const noexcept + { return reconn_.is_cancelled();} private: + using runner_type = detail::runner; + using reconnection_type = detail::basic_reconnection; using this_type = basic_connection; template friend class detail::connection_base; - template friend struct detail::exec_read_op; + template friend class detail::read_next_op; template friend struct detail::exec_op; template friend struct detail::receive_op; template friend struct detail::reader_op; - template friend struct detail::writer_op; - template friend struct detail::run_op; + template friend struct detail::writer_op; + template friend struct detail::run_op; template friend struct detail::wait_receive_op; + template friend struct detail::run_all_op; + template friend struct detail::reconnection_op; + + template + auto async_run_one(Logger l, CompletionToken token) + { return runner_.async_run(*this, l, std::move(token)); } + + template + auto async_run_impl(Logger l, CompletionToken token) + { return base_type::async_run_impl(l, std::move(token)); } + + void close() + { reset_stream(); } - void close() { stream_.close(); } auto is_open() const noexcept { return stream_.is_open(); } auto lowest_layer() noexcept -> auto& { return stream_.lowest_layer(); } + reconnection_type reconn_; + runner_type runner_; Socket stream_; - bool reconnect_; }; /** \brief A connection that uses a asio::ip::tcp::socket. diff --git a/include/boost/redis/detail/connection_base.hpp b/include/boost/redis/detail/connection_base.hpp index b478d13c..7b51608d 100644 --- a/include/boost/redis/detail/connection_base.hpp +++ b/include/boost/redis/detail/connection_base.hpp @@ -8,24 +8,450 @@ #define BOOST_REDIS_CONNECTION_BASE_HPP #include +#include +#include +#include #include #include -#include -#include -#include -#include +#include + +#include #include #include +#include +#include +#include +#include +#include +#include -#include -#include -#include +#include +#include #include +#include +#include #include +#include #include namespace boost::redis::detail { +template +struct wait_receive_op { + Conn* conn; + asio::coroutine coro{}; + + template + void + operator()(Self& self , system::error_code ec = {}) + { + BOOST_ASIO_CORO_REENTER (coro) + { + BOOST_ASIO_CORO_YIELD + conn->channel_.async_send(system::error_code{}, 0, std::move(self)); + BOOST_REDIS_CHECK_OP0(;); + + BOOST_ASIO_CORO_YIELD + conn->channel_.async_send(system::error_code{}, 0, std::move(self)); + BOOST_REDIS_CHECK_OP0(;); + + self.complete({}); + } + } +}; + +template +class read_next_op { +public: + using req_info_type = typename Conn::req_info; + using req_info_ptr = typename std::shared_ptr; + +private: + Conn* conn_; + req_info_ptr info_; + Adapter adapter_; + std::size_t cmds_ = 0; + std::size_t read_size_ = 0; + std::size_t index_ = 0; + asio::coroutine coro_{}; + +public: + read_next_op(Conn& conn, Adapter adapter, req_info_ptr info) + : conn_{&conn} + , info_{info} + , adapter_{adapter} + , cmds_{info->get_number_of_commands()} + {} + + template + void + operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) + { + BOOST_ASIO_CORO_REENTER (coro_) + { + // Loop reading the responses to this request. + while (cmds_ != 0) { + if (info_->stop_requested()) { + self.complete(asio::error::operation_aborted, 0); + return; + } + + //----------------------------------- + // If we detect a push in the middle of a request we have + // to hand it to the push consumer. To do that we need + // some data in the read bufer. + if (conn_->read_buffer_.empty()) { + BOOST_ASIO_CORO_YIELD + asio::async_read_until( + conn_->next_layer(), + conn_->make_dynamic_buffer(), + "\r\n", std::move(self)); + BOOST_REDIS_CHECK_OP1(conn_->cancel(operation::run);); + if (info_->stop_requested()) { + self.complete(asio::error::operation_aborted, 0); + return; + } + } + + // If the next request is a push we have to handle it to + // the receive_op wait for it to be done and continue. + if (resp3::to_type(conn_->read_buffer_.front()) == resp3::type::push) { + BOOST_ASIO_CORO_YIELD + conn_->async_wait_receive(std::move(self)); + BOOST_REDIS_CHECK_OP1(conn_->cancel(operation::run);); + continue; + } + //----------------------------------- + + BOOST_ASIO_CORO_YIELD + redis::detail::async_read( + conn_->next_layer(), + conn_->make_dynamic_buffer(), + [i = index_, adpt = adapter_] (resp3::basic_node const& nd, system::error_code& ec) mutable { adpt(i, nd, ec); }, + std::move(self)); + + ++index_; + + BOOST_REDIS_CHECK_OP1(conn_->cancel(operation::run);); + + read_size_ += n; + + BOOST_ASSERT(cmds_ != 0); + --cmds_; + } + + self.complete({}, read_size_); + } + } +}; + +template +struct receive_op { + Conn* conn; + Adapter adapter; + std::size_t read_size = 0; + asio::coroutine coro{}; + + template + void + operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) + { + BOOST_ASIO_CORO_REENTER (coro) + { + BOOST_ASIO_CORO_YIELD + conn->channel_.async_receive(std::move(self)); + BOOST_REDIS_CHECK_OP1(;); + + BOOST_ASIO_CORO_YIELD + redis::detail::async_read(conn->next_layer(), conn->make_dynamic_buffer(), adapter, std::move(self)); + if (ec || is_cancelled(self)) { + conn->cancel(operation::run); + conn->cancel(operation::receive); + self.complete(!!ec ? ec : asio::error::operation_aborted, {}); + return; + } + + read_size = n; + + BOOST_ASIO_CORO_YIELD + conn->channel_.async_receive(std::move(self)); + BOOST_REDIS_CHECK_OP1(;); + + self.complete({}, read_size); + return; + } + } +}; + +template +struct exec_op { + using req_info_type = typename Conn::req_info; + + Conn* conn = nullptr; + request const* req = nullptr; + Adapter adapter{}; + std::shared_ptr info = nullptr; + std::size_t read_size = 0; + asio::coroutine coro{}; + + template + void + operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) + { + BOOST_ASIO_CORO_REENTER (coro) + { + // Check whether the user wants to wait for the connection to + // be stablished. + if (req->get_config().cancel_if_not_connected && !conn->is_open()) { + BOOST_ASIO_CORO_YIELD + asio::post(std::move(self)); + return self.complete(error::not_connected, 0); + } + + info = std::allocate_shared(asio::get_associated_allocator(self), *req, conn->get_executor()); + + conn->add_request_info(info); +EXEC_OP_WAIT: + BOOST_ASIO_CORO_YIELD + info->async_wait(std::move(self)); + BOOST_ASSERT(ec == asio::error::operation_aborted); + + if (info->stop_requested()) { + // Don't have to call remove_request as it has already + // been by cancel(exec). + return self.complete(ec, 0); + } + + if (is_cancelled(self)) { + if (info->is_written()) { + using c_t = asio::cancellation_type; + auto const c = self.get_cancellation_state().cancelled(); + if ((c & c_t::terminal) != c_t::none) { + // Cancellation requires closing the connection + // otherwise it stays in inconsistent state. + conn->cancel(operation::run); + return self.complete(ec, 0); + } else { + // Can't implement other cancelation types, ignoring. + self.get_cancellation_state().clear(); + goto EXEC_OP_WAIT; + } + } else { + // Cancelation can be honored. + conn->remove_request(info); + self.complete(ec, 0); + return; + } + } + + BOOST_ASSERT(conn->is_open()); + + if (req->size() == 0) { + // Don't have to call remove_request as it has already + // been removed. + return self.complete({}, 0); + } + + BOOST_ASSERT(!conn->reqs_.empty()); + BOOST_ASSERT(conn->reqs_.front() != nullptr); + BOOST_ASIO_CORO_YIELD + conn->async_read_next(adapter, std::move(self)); + BOOST_REDIS_CHECK_OP1(;); + + read_size = n; + + if (info->stop_requested()) { + // Don't have to call remove_request as it has already + // been by cancel(exec). + return self.complete(ec, 0); + } + + BOOST_ASSERT(!conn->reqs_.empty()); + conn->reqs_.pop_front(); + + if (conn->is_waiting_response()) { + BOOST_ASSERT(!conn->reqs_.empty()); + conn->reqs_.front()->proceed(); + } else { + conn->read_timer_.cancel_one(); + } + + self.complete({}, read_size); + } + } +}; + +template +struct run_op { + Conn* conn = nullptr; + Logger logger_; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array order = {} + , system::error_code ec0 = {} + , system::error_code ec1 = {}) + { + BOOST_ASIO_CORO_REENTER (coro) + { + conn->write_buffer_.clear(); + conn->read_buffer_.clear(); + + BOOST_ASIO_CORO_YIELD + asio::experimental::make_parallel_group( + [this](auto token) { return conn->reader(token);}, + [this](auto token) { return conn->writer(logger_, token);} + ).async_wait( + asio::experimental::wait_for_one(), + std::move(self)); + + if (is_cancelled(self)) { + self.complete(asio::error::operation_aborted); + return; + } + + switch (order[0]) { + case 0: self.complete(ec0); break; + case 1: self.complete(ec1); break; + default: BOOST_ASSERT(false); + } + } + } +}; + +template +struct writer_op { + Conn* conn_; + Logger logger_; + asio::coroutine coro{}; + + template + void operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) + { + ignore_unused(n); + + BOOST_ASIO_CORO_REENTER (coro) for (;;) + { + while (conn_->coalesce_requests()) { + BOOST_ASIO_CORO_YIELD + asio::async_write(conn_->next_layer(), asio::buffer(conn_->write_buffer_), std::move(self)); + logger_.on_write(ec, conn_->write_buffer_); + BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);); + + conn_->on_write(); + + // A socket.close() may have been called while a + // successful write might had already been queued, so we + // have to check here before proceeding. + if (!conn_->is_open()) { + self.complete({}); + return; + } + } + + BOOST_ASIO_CORO_YIELD + conn_->writer_timer_.async_wait(std::move(self)); + if (!conn_->is_open() || is_cancelled(self)) { + // Notice this is not an error of the op, stoping was + // requested from the outside, so we complete with + // success. + self.complete({}); + return; + } + } + } +}; + +template +struct reader_op { + Conn* conn; + asio::coroutine coro{}; + + bool as_push() const + { + return + (resp3::to_type(conn->read_buffer_.front()) == resp3::type::push) + || conn->reqs_.empty() + || (!conn->reqs_.empty() && conn->reqs_.front()->get_number_of_commands() == 0) + || !conn->is_waiting_response(); // Added to deal with MONITOR. + } + + template + void operator()( Self& self + , system::error_code ec = {} + , std::size_t n = 0) + { + ignore_unused(n); + + BOOST_ASIO_CORO_REENTER (coro) for (;;) + { + BOOST_ASIO_CORO_YIELD + asio::async_read_until( + conn->next_layer(), + conn->make_dynamic_buffer(), + "\r\n", std::move(self)); + + if (ec == asio::error::eof) { + conn->cancel(operation::run); + return self.complete({}); // EOFINAE: EOF is not an error. + } + + BOOST_REDIS_CHECK_OP0(conn->cancel(operation::run);); + + // We handle unsolicited events in the following way + // + // 1. Its resp3 type is a push. + // + // 2. A non-push type is received with an empty requests + // queue. I have noticed this is possible (e.g. -MISCONF). + // I expect them to have type push so we can distinguish + // them from responses to commands, but it is a + // simple-error. If we are lucky enough to receive them + // when the command queue is empty we can treat them as + // server pushes, otherwise it is impossible to handle + // them properly + // + // 3. The request does not expect any response but we got + // one. This may happen if for example, subscribe with + // wrong syntax. + // + // Useful links: + // + // - https://github.com/redis/redis/issues/11784 + // - https://github.com/redis/redis/issues/6426 + // + BOOST_ASSERT(!conn->read_buffer_.empty()); + if (as_push()) { + BOOST_ASIO_CORO_YIELD + conn->async_wait_receive(std::move(self)); + } else { + BOOST_ASSERT_MSG(conn->is_waiting_response(), "Not waiting for a response (using MONITOR command perhaps?)"); + BOOST_ASSERT(!conn->reqs_.empty()); + BOOST_ASSERT(conn->reqs_.front()->get_number_of_commands() != 0); + conn->reqs_.front()->proceed(); + BOOST_ASIO_CORO_YIELD + conn->read_timer_.async_wait(std::move(self)); + ec = {}; + } + + if (!conn->is_open() || ec || is_cancelled(self)) { + conn->cancel(operation::run); + self.complete(asio::error::basic_errors::operation_aborted); + return; + } + } + } +}; + /** Base class for high level Redis asynchronous connections. * * This class is not meant to be instantiated directly but as base @@ -64,16 +490,14 @@ public: derived().close(); read_timer_.cancel(); writer_timer_.cancel(); - cancel_on_conn_lost(); - - return 1U; + return cancel_on_conn_lost(); } case operation::receive: { channel_.cancel(); return 1U; } - default: BOOST_ASSERT(false); return 0; + default: /* ignore */; return 0; } } @@ -150,7 +574,7 @@ public: return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(redis::detail::exec_op{&derived(), &req, f}, token, writer_timer_); + >(exec_op{&derived(), &req, f}, token, writer_timer_); } template @@ -163,16 +587,16 @@ public: return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(redis::detail::receive_op{&derived(), f}, token, channel_); + >(receive_op{&derived(), f}, token, channel_); } - template - auto async_run(CompletionToken token) + template + auto async_run_impl(Logger l, CompletionToken token) { return asio::async_compose < CompletionToken , void(system::error_code) - >(detail::run_op{&derived()}, token, writer_timer_); + >(run_op{&derived(), l}, token, writer_timer_); } void set_max_buffer_read_size(std::size_t max_read_size) noexcept @@ -296,13 +720,13 @@ private: using reqs_type = std::deque>; - template friend struct detail::reader_op; - template friend struct detail::writer_op; - template friend struct detail::run_op; - template friend struct detail::exec_op; - template friend struct detail::exec_read_op; - template friend struct detail::receive_op; - template friend struct detail::wait_receive_op; + template friend struct reader_op; + template friend struct writer_op; + template friend struct run_op; + template friend struct exec_op; + template friend class read_next_op; + template friend struct receive_op; + template friend struct wait_receive_op; template auto async_wait_receive(CompletionToken token) @@ -356,25 +780,25 @@ private: return asio::async_compose < CompletionToken , void(system::error_code) - >(detail::reader_op{&derived()}, token, writer_timer_); + >(reader_op{&derived()}, token, writer_timer_); } - template - auto writer(CompletionToken&& token) + template + auto writer(Logger l, CompletionToken&& token) { return asio::async_compose < CompletionToken , void(system::error_code) - >(detail::writer_op{&derived()}, token, writer_timer_); + >(writer_op{&derived(), l}, token, writer_timer_); } template - auto async_exec_read(Adapter adapter, std::size_t cmds, CompletionToken token) + auto async_read_next(Adapter adapter, CompletionToken token) { return asio::async_compose < CompletionToken , void(system::error_code, std::size_t) - >(detail::exec_read_op{&derived(), adapter, cmds}, token, writer_timer_); + >(read_next_op{derived(), adapter, reqs_.front()}, token, writer_timer_); } [[nodiscard]] bool coalesce_requests() diff --git a/include/boost/redis/detail/connection_ops.hpp b/include/boost/redis/detail/connection_ops.hpp deleted file mode 100644 index 1452341b..00000000 --- a/include/boost/redis/detail/connection_ops.hpp +++ /dev/null @@ -1,403 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_CONNECTION_OPS_HPP -#define BOOST_REDIS_CONNECTION_OPS_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace boost::redis::detail { - -template -struct wait_receive_op { - Conn* conn; - asio::coroutine coro{}; - - template - void - operator()(Self& self , system::error_code ec = {}) - { - BOOST_ASIO_CORO_REENTER (coro) - { - BOOST_ASIO_CORO_YIELD - conn->channel_.async_send(system::error_code{}, 0, std::move(self)); - BOOST_REDIS_CHECK_OP0(;); - - BOOST_ASIO_CORO_YIELD - conn->channel_.async_send(system::error_code{}, 0, std::move(self)); - BOOST_REDIS_CHECK_OP0(;); - - self.complete({}); - } - } -}; - -template -struct exec_read_op { - Conn* conn; - Adapter adapter; - std::size_t cmds = 0; - std::size_t read_size = 0; - std::size_t index = 0; - asio::coroutine coro{}; - - template - void - operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - BOOST_ASIO_CORO_REENTER (coro) - { - // Loop reading the responses to this request. - BOOST_ASSERT(!conn->reqs_.empty()); - while (cmds != 0) { - BOOST_ASSERT(conn->is_waiting_response()); - - //----------------------------------- - // If we detect a push in the middle of a request we have - // to hand it to the push consumer. To do that we need - // some data in the read bufer. - if (conn->read_buffer_.empty()) { - BOOST_ASIO_CORO_YIELD - asio::async_read_until( - conn->next_layer(), - conn->make_dynamic_buffer(), - "\r\n", std::move(self)); - BOOST_REDIS_CHECK_OP1(conn->cancel(operation::run);); - } - - // If the next request is a push we have to handle it to - // the receive_op wait for it to be done and continue. - if (resp3::to_type(conn->read_buffer_.front()) == resp3::type::push) { - BOOST_ASIO_CORO_YIELD - conn->async_wait_receive(std::move(self)); - BOOST_REDIS_CHECK_OP1(conn->cancel(operation::run);); - continue; - } - //----------------------------------- - - BOOST_ASIO_CORO_YIELD - redis::detail::async_read( - conn->next_layer(), - conn->make_dynamic_buffer(), - [i = index, adpt = adapter] (resp3::basic_node const& nd, system::error_code& ec) mutable { adpt(i, nd, ec); }, - std::move(self)); - - ++index; - - BOOST_REDIS_CHECK_OP1(conn->cancel(operation::run);); - - read_size += n; - - BOOST_ASSERT(cmds != 0); - --cmds; - } - - self.complete({}, read_size); - } - } -}; - -template -struct receive_op { - Conn* conn; - Adapter adapter; - std::size_t read_size = 0; - asio::coroutine coro{}; - - template - void - operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - BOOST_ASIO_CORO_REENTER (coro) - { - BOOST_ASIO_CORO_YIELD - conn->channel_.async_receive(std::move(self)); - BOOST_REDIS_CHECK_OP1(;); - - BOOST_ASIO_CORO_YIELD - redis::detail::async_read(conn->next_layer(), conn->make_dynamic_buffer(), adapter, std::move(self)); - if (ec || is_cancelled(self)) { - conn->cancel(operation::run); - conn->cancel(operation::receive); - self.complete(!!ec ? ec : asio::error::operation_aborted, {}); - return; - } - - read_size = n; - - BOOST_ASIO_CORO_YIELD - conn->channel_.async_receive(std::move(self)); - BOOST_REDIS_CHECK_OP1(;); - - self.complete({}, read_size); - return; - } - } -}; - -template -struct exec_op { - using req_info_type = typename Conn::req_info; - - Conn* conn = nullptr; - request const* req = nullptr; - Adapter adapter{}; - std::shared_ptr info = nullptr; - std::size_t read_size = 0; - asio::coroutine coro{}; - - template - void - operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - BOOST_ASIO_CORO_REENTER (coro) - { - // Check whether the user wants to wait for the connection to - // be stablished. - if (req->get_config().cancel_if_not_connected && !conn->is_open()) { - return self.complete(error::not_connected, 0); - } - - info = std::allocate_shared(asio::get_associated_allocator(self), *req, conn->get_executor()); - - conn->add_request_info(info); -EXEC_OP_WAIT: - BOOST_ASIO_CORO_YIELD - info->async_wait(std::move(self)); - BOOST_ASSERT(ec == asio::error::operation_aborted); - - if (info->stop_requested()) { - // Don't have to call remove_request as it has already - // been by cancel(exec). - return self.complete(ec, 0); - } - - if (is_cancelled(self)) { - if (info->is_written()) { - using c_t = asio::cancellation_type; - auto const c = self.get_cancellation_state().cancelled(); - if ((c & c_t::terminal) != c_t::none) { - // Cancellation requires closing the connection - // otherwise it stays in inconsistent state. - conn->cancel(operation::run); - return self.complete(ec, 0); - } else { - // Can't implement other cancelation types, ignoring. - self.get_cancellation_state().clear(); - goto EXEC_OP_WAIT; - } - } else { - // Cancelation can be honored. - conn->remove_request(info); - self.complete(ec, 0); - return; - } - } - - BOOST_ASSERT(conn->is_open()); - - if (req->size() == 0) { - // Don't have to call remove_request as it has already - // been removed. - return self.complete({}, 0); - } - - BOOST_ASSERT(!conn->reqs_.empty()); - BOOST_ASSERT(conn->reqs_.front() != nullptr); - BOOST_ASIO_CORO_YIELD - conn->async_exec_read(adapter, conn->reqs_.front()->get_number_of_commands(), std::move(self)); - BOOST_REDIS_CHECK_OP1(;); - - read_size = n; - - BOOST_ASSERT(!conn->reqs_.empty()); - conn->reqs_.pop_front(); - - if (conn->is_waiting_response()) { - BOOST_ASSERT(!conn->reqs_.empty()); - conn->reqs_.front()->proceed(); - } else { - conn->read_timer_.cancel_one(); - } - - self.complete({}, read_size); - } - } -}; - -template -struct run_op { - Conn* conn = nullptr; - asio::coroutine coro{}; - - template - void operator()( Self& self - , std::array order = {} - , system::error_code ec0 = {} - , system::error_code ec1 = {}) - { - BOOST_ASIO_CORO_REENTER (coro) - { - conn->write_buffer_.clear(); - conn->read_buffer_.clear(); - - BOOST_ASIO_CORO_YIELD - asio::experimental::make_parallel_group( - [this](auto token) { return conn->reader(token);}, - [this](auto token) { return conn->writer(token);} - ).async_wait( - asio::experimental::wait_for_one(), - std::move(self)); - - if (is_cancelled(self)) { - self.complete(asio::error::operation_aborted); - return; - } - - switch (order[0]) { - case 0: self.complete(ec0); break; - case 1: self.complete(ec1); break; - default: BOOST_ASSERT(false); - } - } - } -}; - -template -struct writer_op { - Conn* conn; - asio::coroutine coro{}; - - template - void operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - ignore_unused(n); - - BOOST_ASIO_CORO_REENTER (coro) for (;;) - { - while (conn->coalesce_requests()) { - BOOST_ASIO_CORO_YIELD - asio::async_write(conn->next_layer(), asio::buffer(conn->write_buffer_), std::move(self)); - BOOST_REDIS_CHECK_OP0(conn->cancel(operation::run);); - - conn->on_write(); - - // A socket.close() may have been called while a - // successful write might had already been queued, so we - // have to check here before proceeding. - if (!conn->is_open()) { - self.complete({}); - return; - } - } - - BOOST_ASIO_CORO_YIELD - conn->writer_timer_.async_wait(std::move(self)); - if (!conn->is_open() || is_cancelled(self)) { - // Notice this is not an error of the op, stoping was - // requested from the outside, so we complete with - // success. - self.complete({}); - return; - } - } - } -}; - -template -struct reader_op { - Conn* conn; - asio::coroutine coro{}; - - template - void operator()( Self& self - , system::error_code ec = {} - , std::size_t n = 0) - { - ignore_unused(n); - - BOOST_ASIO_CORO_REENTER (coro) for (;;) - { - BOOST_ASIO_CORO_YIELD - asio::async_read_until( - conn->next_layer(), - conn->make_dynamic_buffer(), - "\r\n", std::move(self)); - - if (ec == asio::error::eof) { - conn->cancel(operation::run); - return self.complete({}); // EOFINAE: EOF is not an error. - } - - BOOST_REDIS_CHECK_OP0(conn->cancel(operation::run);); - - // We handle unsolicited events in the following way - // - // 1. Its resp3 type is a push. - // - // 2. A non-push type is received with an empty requests - // queue. I have noticed this is possible (e.g. -MISCONF). - // I expect them to have type push so we can distinguish - // them from responses to commands, but it is a - // simple-error. If we are lucky enough to receive them - // when the command queue is empty we can treat them as - // server pushes, otherwise it is impossible to handle - // them properly - // - // 3. The request does not expect any response but we got - // one. This may happen if for example, subscribe with - // wrong syntax. - // - BOOST_ASSERT(!conn->read_buffer_.empty()); - if (resp3::to_type(conn->read_buffer_.front()) == resp3::type::push - || conn->reqs_.empty() - || (!conn->reqs_.empty() && conn->reqs_.front()->get_number_of_commands() == 0)) { - BOOST_ASIO_CORO_YIELD - conn->async_wait_receive(std::move(self)); - } else { - BOOST_ASSERT(conn->is_waiting_response()); - BOOST_ASSERT(!conn->reqs_.empty()); - BOOST_ASSERT(conn->reqs_.front()->get_number_of_commands() != 0); - conn->reqs_.front()->proceed(); - BOOST_ASIO_CORO_YIELD - conn->read_timer_.async_wait(std::move(self)); - ec = {}; - } - - if (!conn->is_open() || ec || is_cancelled(self)) { - conn->cancel(operation::run); - self.complete(asio::error::basic_errors::operation_aborted); - return; - } - } - } -}; - -} // boost::redis::detail - -#endif // BOOST_REDIS_CONNECTION_OPS_HPP diff --git a/include/boost/redis/detail/connector.hpp b/include/boost/redis/detail/connector.hpp new file mode 100644 index 00000000..4e9c1507 --- /dev/null +++ b/include/boost/redis/detail/connector.hpp @@ -0,0 +1,133 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_CONNECTOR_HPP +#define BOOST_REDIS_CONNECTOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct connect_op { + Connector* ctor_ = nullptr; + Stream* stream = nullptr; + asio::ip::tcp::resolver::results_type const* res_ = nullptr; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array const& order = {} + , system::error_code const& ec1 = {} + , asio::ip::tcp::endpoint const& ep= {} + , system::error_code const& ec2 = {}) + { + BOOST_ASIO_CORO_REENTER (coro) + { + ctor_->timer_.expires_after(ctor_->timeout_); + + BOOST_ASIO_CORO_YIELD + asio::experimental::make_parallel_group( + [this](auto token) + { + auto f = [](system::error_code const&, auto const&) { return true; }; + return asio::async_connect(*stream, *res_, f, token); + }, + [this](auto token) { return ctor_->timer_.async_wait(token);} + ).async_wait( + asio::experimental::wait_for_one(), + std::move(self)); + + if (is_cancelled(self)) { + self.complete(asio::error::operation_aborted); + return; + } + + switch (order[0]) { + case 0: { + ctor_->endpoint_ = ep; + self.complete(ec1); + } break; + case 1: + { + if (ec2) { + self.complete(ec2); + } else { + self.complete(error::connect_timeout); + } + } break; + + default: BOOST_ASSERT(false); + } + } + } +}; + +template +class connector { +public: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + connector(Executor ex) + : timer_{ex} + {} + + void set_config(config const& cfg) + { timeout_ = cfg.connect_timeout; } + + template + auto + async_connect( + Stream& stream, + asio::ip::tcp::resolver::results_type const& res, + CompletionToken&& token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(connect_op{this, &stream, &res}, token, timer_); + } + + std::size_t cancel(operation op) + { + switch (op) { + case operation::connect: + case operation::all: + timer_.cancel(); + break; + default: /* ignore */; + } + + return 0; + } + + auto const& endpoint() const noexcept { return endpoint_;} + +private: + template friend struct connect_op; + + timer_type timer_; + std::chrono::steady_clock::duration timeout_ = std::chrono::seconds{2}; + asio::ip::tcp::endpoint endpoint_; +}; + +} // boost::redis::detail + +#endif // BOOST_REDIS_CONNECTOR_HPP diff --git a/include/boost/redis/check_health.hpp b/include/boost/redis/detail/health_checker.hpp similarity index 71% rename from include/boost/redis/check_health.hpp rename to include/boost/redis/detail/health_checker.hpp index aaf6d46b..374f2b87 100644 --- a/include/boost/redis/check_health.hpp +++ b/include/boost/redis/detail/health_checker.hpp @@ -4,14 +4,15 @@ * accompanying file LICENSE.txt) */ -#ifndef BOOST_REDIS_CHECK_HEALTH_HPP -#define BOOST_REDIS_CHECK_HEALTH_HPP +#ifndef BOOST_REDIS_HEALTH_CHECKER_HPP +#define BOOST_REDIS_HEALTH_CHECKER_HPP // Has to included before promise.hpp to build on msvc. #include #include #include #include +#include #include #include #include @@ -20,8 +21,7 @@ #include #include -namespace boost::redis { -namespace detail { +namespace boost::redis::detail { template class ping_op { @@ -70,7 +70,7 @@ public: checker_->wait_timer_.async_wait(std::move(self)); BOOST_REDIS_CHECK_OP0(;) - if (!checker_->resp_.has_value()) { + if (checker_->resp_.has_error()) { self.complete({}); return; } @@ -83,8 +83,6 @@ public: return; } - checker_->resp_.value().clear(); - if (checker_->resp_.has_value()) { checker_->resp_.value().clear(); } @@ -109,6 +107,13 @@ public: { BOOST_ASIO_CORO_REENTER (coro_) { + if (checker_->ping_interval_.count() == 0) { + BOOST_ASIO_CORO_YIELD + asio::post(std::move(self)); + self.complete({}); + return; + } + BOOST_ASIO_CORO_YIELD asio::experimental::make_parallel_group( [this](auto token) { return checker_->async_ping(*conn_, token); }, @@ -141,15 +146,18 @@ private: Executor>; public: - health_checker( - Executor ex, - std::string const& msg, - std::chrono::steady_clock::duration ping_interval) + health_checker(Executor ex) : ping_timer_{ex} , wait_timer_{ex} - , ping_interval_{ping_interval} { - req_.push("PING", msg); + req_.push("PING", "Boost.Redis"); + } + + void set_config(config const& cfg) + { + req_.clear(); + req_.push("PING", cfg.health_check_id); + ping_interval_ = cfg.health_check_interval; } template < @@ -164,10 +172,18 @@ public: >(check_health_op{this, &conn}, token, conn); } - void cancel() + std::size_t cancel(operation op) { - ping_timer_.cancel(); - wait_timer_.cancel(); + switch (op) { + case operation::health_check: + case operation::all: + ping_timer_.cancel(); + wait_timer_.cancel(); + break; + default: /* ignore */; + } + + return 0; } private: @@ -197,50 +213,10 @@ private: timer_type wait_timer_; redis::request req_; redis::generic_response resp_; - std::chrono::steady_clock::duration ping_interval_; + std::chrono::steady_clock::duration ping_interval_ = std::chrono::seconds{5}; bool checker_has_exited_ = false; }; -} // detail +} // boost::redis::detail -/** @brief Checks Redis health asynchronously - * @ingroup high-level-api - * - * This function will ping the Redis server periodically until a ping - * timesout or an error occurs. On timeout this function will - * complete with success. - * - * @param conn A connection to the Redis server. - * @param msg The message to be sent with the [PING](https://redis.io/commands/ping/) command. Seting a proper and unique id will help users identify which connections are active. - * @param ping_interval Ping ping_interval. - * @param token The completion token - * - * The completion token must have the following signature - * - * @code - * void f(system::error_code); - * @endcode - * - * Completion occurs when a pong response is not receive within two - * times the ping interval. - */ -template < - class Connection, - class CompletionToken = asio::default_completion_token_t -> -auto -async_check_health( - Connection& conn, - std::string const& msg = "Boost.Redis", - std::chrono::steady_clock::duration ping_interval = std::chrono::seconds{2}, - CompletionToken token = CompletionToken{}) -{ - using executor_type = typename Connection::executor_type; - using health_checker_type = detail::health_checker; - auto checker = std::make_shared(conn.get_executor(), msg, ping_interval); - return checker->async_check_health(conn, asio::consign(std::move(token), checker)); -} - -} // boost::redis - -#endif // BOOST_REDIS_CHECK_HEALTH_HPP +#endif // BOOST_REDIS_HEALTH_CHECKER_HPP diff --git a/include/boost/redis/detail/reconnection.hpp b/include/boost/redis/detail/reconnection.hpp new file mode 100644 index 00000000..d021a3d9 --- /dev/null +++ b/include/boost/redis/detail/reconnection.hpp @@ -0,0 +1,135 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_RECONNECTION_HPP +#define BOOST_REDIS_RECONNECTION_HPP + +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct reconnection_op { + Reconnector* reconn_ = nullptr; + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; + + template + void operator()(Self& self, system::error_code ec = {}) + { + BOOST_ASIO_CORO_REENTER (coro_) for (;;) + { + BOOST_ASIO_CORO_YIELD + conn_->async_run_one(logger_, std::move(self)); + conn_->reset_stream(); + conn_->cancel(operation::receive); + logger_.on_connection_lost(ec); + if (reconn_->is_cancelled() || is_cancelled(self)) { + reconn_->cancel(operation::reconnection); + self.complete(!!ec ? ec : asio::error::operation_aborted); + return; + } + + reconn_->timer_.expires_after(reconn_->wait_interval_); + BOOST_ASIO_CORO_YIELD + reconn_->timer_.async_wait(std::move(self)); + BOOST_REDIS_CHECK_OP0(;) + if (reconn_->is_cancelled()) { + self.complete(asio::error::operation_aborted); + return; + } + } + } +}; + +// NOTE: wait_interval could be an async_run parameter. + +template +class basic_reconnection { +public: + /// Executor type. + using executor_type = Executor; + + basic_reconnection(Executor ex) + : timer_{ex} + , is_cancelled_{false} + {} + + basic_reconnection(asio::io_context& ioc, std::chrono::steady_clock::duration wait_interval) + : basic_reconnection{ioc.get_executor(), wait_interval} + {} + + /// Rebinds to a new executor type. + template + struct rebind_executor + { + using other = basic_reconnection; + }; + + template < + class Connection, + class Logger = logger, + class CompletionToken = asio::default_completion_token_t + > + auto + async_run( + Connection& conn, + Logger l = Logger{}, + CompletionToken token = CompletionToken{}) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(detail::reconnection_op{this, &conn, l}, token, conn); + } + + void set_wait_interval(std::chrono::steady_clock::duration wait_interval) + { + wait_interval_ = wait_interval; + } + + std::size_t cancel(operation op) + { + switch (op) { + case operation::reconnection: + case operation::all: + is_cancelled_ = true; + timer_.cancel(); + break; + default: /* ignore */; + } + + return 0U; + } + + bool is_cancelled() const noexcept {return is_cancelled_;} + void reset() noexcept {is_cancelled_ = false;} + +private: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + template friend struct detail::reconnection_op; + + timer_type timer_; + std::chrono::steady_clock::duration wait_interval_ = std::chrono::seconds{1}; + bool is_cancelled_; +}; + +using reconnection = basic_reconnection; + +} // boost::redis + +#endif // BOOST_REDIS_RECONNECTION_HPP diff --git a/include/boost/redis/detail/resolver.hpp b/include/boost/redis/detail/resolver.hpp new file mode 100644 index 00000000..f4a31036 --- /dev/null +++ b/include/boost/redis/detail/resolver.hpp @@ -0,0 +1,137 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_RESOLVER_HPP +#define BOOST_REDIS_RESOLVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::detail +{ + +template +struct resolve_op { + Resolver* resv_ = nullptr; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array order = {} + , system::error_code ec1 = {} + , asio::ip::tcp::resolver::results_type res = {} + , system::error_code ec2 = {}) + { + BOOST_ASIO_CORO_REENTER (coro) + { + resv_->timer_.expires_after(resv_->timeout_); + + BOOST_ASIO_CORO_YIELD + asio::experimental::make_parallel_group( + [this](auto token) + { + return resv_->resv_.async_resolve(resv_->addr_.host, resv_->addr_.port, token); + }, + [this](auto token) { return resv_->timer_.async_wait(token);} + ).async_wait( + asio::experimental::wait_for_one(), + std::move(self)); + + if (is_cancelled(self)) { + self.complete(asio::error::operation_aborted); + return; + } + + switch (order[0]) { + case 0: { + // Resolver completed first. + resv_->results_ = res; + self.complete(ec1); + } break; + + case 1: { + if (ec2) { + // Timer completed first with error, perhaps a + // cancellation going on. + self.complete(ec2); + } else { + // Timer completed first without an error, this is a + // resolve timeout. + self.complete(error::resolve_timeout); + } + } break; + + default: BOOST_ASSERT(false); + } + } + } +}; + +template +class resolver { +public: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + resolver(Executor ex) : resv_{ex} , timer_{ex} {} + + template + auto async_resolve(CompletionToken&& token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(resolve_op{this}, token, resv_); + } + + std::size_t cancel(operation op) + { + switch (op) { + case operation::resolve: + case operation::all: + resv_.cancel(); + timer_.cancel(); + break; + default: /* ignore */; + } + + return 0; + } + + auto const& results() const noexcept + { return results_;} + + void set_config(config const& cfg) + { + addr_ = cfg.addr; + timeout_ = cfg.resolve_timeout; + } + +private: + using resolver_type = asio::ip::basic_resolver; + template friend struct resolve_op; + + resolver_type resv_; + timer_type timer_; + address addr_; + std::chrono::steady_clock::duration timeout_; + asio::ip::tcp::resolver::results_type results_; +}; + +} // boost::redis::detail + +#endif // BOOST_REDIS_RESOLVER_HPP diff --git a/include/boost/redis/detail/runner.hpp b/include/boost/redis/detail/runner.hpp index 6a86d219..b9329187 100644 --- a/include/boost/redis/detail/runner.hpp +++ b/include/boost/redis/detail/runner.hpp @@ -7,229 +7,238 @@ #ifndef BOOST_REDIS_RUNNER_HPP #define BOOST_REDIS_RUNNER_HPP -// Has to included before promise.hpp to build on msvc. +#include +#include +#include #include #include -#include +#include +#include +#include +#include #include -#include -#include #include #include #include #include +#include +#include #include #include -namespace boost::redis::detail { +namespace boost::redis::detail +{ -template -struct resolve_op { - Runner* runner = nullptr; - asio::coroutine coro{}; +template +struct hello_op { + Runner* runner_ = nullptr; + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; template - void operator()( Self& self - , std::array order = {} - , system::error_code ec1 = {} - , asio::ip::tcp::resolver::results_type res = {} - , system::error_code ec2 = {}) + void operator()(Self& self, system::error_code ec = {}, std::size_t = 0) { - BOOST_ASIO_CORO_REENTER (coro) + BOOST_ASIO_CORO_REENTER (coro_) { + runner_->hello_req_.clear(); + if (runner_->hello_resp_.has_value()) + runner_->hello_resp_.value().clear(); + runner_->add_hello(); + BOOST_ASIO_CORO_YIELD - asio::experimental::make_parallel_group( - [this](auto token) - { - return runner->resv_.async_resolve(runner->addr_.host, runner->addr_.port, token); - }, - [this](auto token) { return runner->timer_.async_wait(token);} - ).async_wait( - asio::experimental::wait_for_one(), - std::move(self)); - - runner->logger_.on_resolve(ec1, res); - - if (is_cancelled(self)) { - self.complete(asio::error::operation_aborted); - return; - } - - switch (order[0]) { - case 0: { - // Resolver completed first. - runner->endpoints_ = res; - self.complete(ec1); - } break; - - case 1: { - if (ec2) { - // Timer completed first with error, perhaps a - // cancellation going on. - self.complete(ec2); - } else { - // Timer completed first without an error, this is a - // resolve timeout. - self.complete(error::resolve_timeout); - } - } break; - - default: BOOST_ASSERT(false); - } + conn_->async_exec(runner_->hello_req_, runner_->hello_resp_, std::move(self)); + logger_.on_hello(ec, runner_->hello_resp_); + BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);) + self.complete(ec); } } }; -template -struct connect_op { - Runner* runner = nullptr; - Stream* stream = nullptr; - asio::coroutine coro{}; +template +class runner_op { +private: + Runner* runner_ = nullptr; + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; - template - void operator()( Self& self - , std::array const& order = {} - , system::error_code const& ec1 = {} - , asio::ip::tcp::endpoint const& ep= {} - , system::error_code const& ec2 = {}) - { - BOOST_ASIO_CORO_REENTER (coro) - { - BOOST_ASIO_CORO_YIELD - asio::experimental::make_parallel_group( - [this](auto token) - { - auto f = [](system::error_code const&, auto const&) { return true; }; - return asio::async_connect(*stream, runner->endpoints_, f, token); - }, - [this](auto token) { return runner->timer_.async_wait(token);} - ).async_wait( - asio::experimental::wait_for_one(), - std::move(self)); - - runner->logger_.on_connect(ec1, ep); - - if (is_cancelled(self)) { - self.complete(asio::error::operation_aborted); - return; - } - - switch (order[0]) { - case 0: { - self.complete(ec1); - } break; - case 1: - { - if (ec2) { - self.complete(ec2); - } else { - self.complete(error::connect_timeout); - } - } break; - - default: BOOST_ASSERT(false); - } - } - } -}; - -template -struct runner_op { - Runner* runner = nullptr; - Connection* conn = nullptr; - std::chrono::steady_clock::duration resolve_timeout; - std::chrono::steady_clock::duration connect_timeout; - asio::coroutine coro{}; - - template - void operator()(Self& self, system::error_code ec = {}) - { - BOOST_ASIO_CORO_REENTER (coro) - { - runner->timer_.expires_after(resolve_timeout); - BOOST_ASIO_CORO_YIELD - runner->async_resolve(std::move(self)); - BOOST_REDIS_CHECK_OP0(;) - - runner->timer_.expires_after(connect_timeout); - BOOST_ASIO_CORO_YIELD - runner->async_connect(conn->next_layer(), std::move(self)); - BOOST_REDIS_CHECK_OP0(;) - - BOOST_ASIO_CORO_YIELD - conn->async_run(std::move(self)); - BOOST_REDIS_CHECK_OP0(;) - self.complete({}); - } - } -}; - -template -class runner { public: - using timer_type = - asio::basic_waitable_timer< - std::chrono::steady_clock, - asio::wait_traits, - Executor>; - - runner(Executor ex, address addr, Logger l = Logger{}) - : resv_{ex} - , timer_{ex} - , addr_{addr} + runner_op(Runner* runner, Connection* conn, Logger l) + : runner_{runner} + , conn_{conn} , logger_{l} {} - template - auto async_resolve(CompletionToken&& token) + template + void operator()( Self& self + , std::array order = {} + , system::error_code ec0 = {} + , system::error_code ec1 = {} + , system::error_code ec2 = {} + , std::size_t = 0) + { + BOOST_ASIO_CORO_REENTER (coro_) + { + BOOST_ASIO_CORO_YIELD + asio::experimental::make_parallel_group( + [this](auto token) { return runner_->async_run_all(*conn_, logger_, token); }, + [this](auto token) { return runner_->health_checker_.async_check_health(*conn_, token); }, + [this](auto token) { return runner_->async_hello(*conn_, logger_, token); } + ).async_wait( + asio::experimental::wait_for_all(), + std::move(self)); + + if (is_cancelled(self)) { + self.complete(asio::error::operation_aborted); + return; + } + + if (ec0 == error::connect_timeout || ec0 == error::resolve_timeout) { + self.complete(ec0); + return; + } + + if (order[0] == 2 && !!ec2) { + self.complete(ec2); + return; + } + + if (order[0] == 1 && ec1 == error::pong_timeout) { + self.complete(ec1); + return; + } + + self.complete(ec0); + } + } +}; + +template +struct run_all_op { + Runner* runner_ = nullptr; + Connection* conn_ = nullptr; + Logger logger_; + asio::coroutine coro_{}; + + template + void operator()(Self& self, system::error_code ec = {}, std::size_t = 0) + { + BOOST_ASIO_CORO_REENTER (coro_) + { + BOOST_ASIO_CORO_YIELD + runner_->resv_.async_resolve(std::move(self)); + logger_.on_resolve(ec, runner_->resv_.results()); + BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);) + + BOOST_ASIO_CORO_YIELD + runner_->ctor_.async_connect(conn_->lowest_layer(), runner_->resv_.results(), std::move(self)); + logger_.on_connect(ec, runner_->ctor_.endpoint()); + BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);) + + if (!runner_->hsher_.is_dummy()) { + BOOST_ASIO_CORO_YIELD + runner_->hsher_.async_handshake(conn_->next_layer(), std::move(self)); + logger_.on_ssl_handshake(ec); + BOOST_REDIS_CHECK_OP0(conn_->cancel(operation::run);) + } + + BOOST_ASIO_CORO_YIELD + conn_->async_run_impl(logger_, std::move(self)); + BOOST_REDIS_CHECK_OP0(;) + self.complete(ec); + } + } +}; + +template class Handshaker> +class runner { +public: + runner(Executor ex, config cfg) + : resv_{ex} + , ctor_{ex} + , hsher_{ex} + , health_checker_{ex} + , cfg_{cfg} + { } + + std::size_t cancel(operation op) + { + resv_.cancel(op); + ctor_.cancel(op); + hsher_.cancel(op); + health_checker_.cancel(op); + return 0U; + } + + void set_config(config const& cfg) + { + cfg_ = cfg; + resv_.set_config(cfg); + ctor_.set_config(cfg); + hsher_.set_config(cfg); + health_checker_.set_config(cfg); + } + + template + auto async_run(Connection& conn, Logger l, CompletionToken token) { return asio::async_compose < CompletionToken , void(system::error_code) - >(resolve_op{this}, token, resv_); + >(runner_op{this, &conn, l}, token, conn); } - template - auto async_connect(Stream& stream, CompletionToken&& token) - { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(connect_op{this, &stream}, token, resv_); - } - - template - auto - async_run( - Connection& conn, - std::chrono::steady_clock::duration resolve_timeout, - std::chrono::steady_clock::duration connect_timeout, - CompletionToken&& token) - { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(runner_op{this, &conn, resolve_timeout, connect_timeout}, token, resv_); - } - - void cancel() - { - resv_.cancel(); - timer_.cancel(); - } + config const& get_config() const noexcept {return cfg_;} private: - using resolver_type = asio::ip::basic_resolver; + using resolver_type = resolver; + using connector_type = connector; + using handshaker_type = Handshaker; + using health_checker_type = health_checker; + using timer_type = typename connector_type::timer_type; - template friend struct runner_op; - template friend struct connect_op; - template friend struct resolve_op; + template friend struct run_all_op; + template friend class runner_op; + template friend struct hello_op; + + template + auto async_run_all(Connection& conn, Logger l, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(run_all_op{this, &conn, l}, token, conn); + } + + template + auto async_hello(Connection& conn, Logger l, CompletionToken token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(hello_op{this, &conn, l}, token, conn); + } + + void add_hello() + { + if (!cfg_.username.empty() && !cfg_.password.empty() && !cfg_.clientname.empty()) + hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password, "SETNAME", cfg_.clientname); + else if (cfg_.username.empty() && cfg_.password.empty() && cfg_.clientname.empty()) + hello_req_.push("HELLO", "3"); + else if (cfg_.clientname.empty()) + hello_req_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password); + else + hello_req_.push("HELLO", "3", "SETNAME", cfg_.clientname); + } resolver_type resv_; - timer_type timer_; - address addr_; - asio::ip::tcp::resolver::results_type endpoints_; - Logger logger_; + connector_type ctor_; + handshaker_type hsher_; + health_checker_type health_checker_; + request hello_req_; + generic_response hello_resp_; + config cfg_; }; } // boost::redis::detail diff --git a/include/boost/redis/error.hpp b/include/boost/redis/error.hpp index 0cc18fa1..7424aea7 100644 --- a/include/boost/redis/error.hpp +++ b/include/boost/redis/error.hpp @@ -72,6 +72,9 @@ enum class error /// Connect timeout pong_timeout, + + /// SSL handshake timeout + ssl_handshake_timeout, }; /** \internal diff --git a/include/boost/redis/experimental/connector.hpp b/include/boost/redis/experimental/connector.hpp deleted file mode 100644 index 13dbce97..00000000 --- a/include/boost/redis/experimental/connector.hpp +++ /dev/null @@ -1,313 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_CONNECTOR_HPP -#define BOOST_REDIS_CONNECTOR_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost::redis::experimental -{ - -struct connect_config { - address addr = address{"127.0.0.1", "6379"}; - std::string username; - std::string password; - std::string clientname = "Boost.Redis"; - std::string health_check_id = "Boost.Redis"; - std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10}; - std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10}; - std::chrono::steady_clock::duration health_check_timeout = std::chrono::seconds{2}; - std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1}; -}; - -namespace detail -{ - -template -struct hello_op { - Connector* ctor_ = nullptr; - Connection* conn_ = nullptr; - asio::coroutine coro_{}; - - template - void operator()(Self& self, system::error_code ec = {}, std::size_t = 0) - { - BOOST_ASIO_CORO_REENTER (coro_) - { - ctor_->req_hello_.clear(); - ctor_->resp_hello_.value().clear(); - ctor_->add_hello(); - - BOOST_ASIO_CORO_YIELD - conn_->async_exec(ctor_->req_hello_, ctor_->resp_hello_, std::move(self)); - - ctor_->logger_.on_hello(ec); - - BOOST_REDIS_CHECK_OP0(conn_->cancel(redis::operation::run);) - - if (ctor_->resp_hello_.has_error()) { - conn_->cancel(redis::operation::run); - switch (ctor_->resp_hello_.error().data_type) { - case resp3::type::simple_error: - self.complete(error::resp3_simple_error); - break; - - case resp3::type::blob_error: - self.complete(error::resp3_blob_error); - break; - - default: BOOST_ASSERT_MSG(false, "Unexpects error data type."); - } - } else { - self.complete({}); - } - } - } -}; - -template -struct run_check_exec_op { - Connector* ctor_ = nullptr; - Connection* conn_ = nullptr; - asio::coroutine coro_{}; - - template - void operator()( Self& self - , std::array order = {} - , system::error_code ec1 = {} - , system::error_code ec2 = {} - , std::size_t = 0) - { - BOOST_ASIO_CORO_REENTER (coro_) - { - BOOST_ASIO_CORO_YIELD - asio::experimental::make_parallel_group( - [this](auto token) { return ctor_->async_run_check(*conn_, token); }, - [this](auto token) { return ctor_->async_hello(*conn_, token); } - ).async_wait( - asio::experimental::wait_for_all(), - std::move(self) - ); - - if (is_cancelled(self)) { - self.complete(asio::error::operation_aborted); - return; - } - - // TODO: Which op should we use to complete? - switch (order[0]) { - case 0: self.complete(ec1); break; - case 1: self.complete(ec2); break; - default: BOOST_ASSERT(false); - } - } - } -}; - -template -struct run_check_op { - Connector* ctor_ = nullptr; - Connection* conn_ = nullptr; - asio::coroutine coro_{}; - - template - void operator()( Self& self - , std::array order = {} - , system::error_code ec1 = {} - , system::error_code ec2 = {}) - { - BOOST_ASIO_CORO_REENTER (coro_) - { - BOOST_ASIO_CORO_YIELD - asio::experimental::make_parallel_group( - [this](auto token) - { - return ctor_->runner_.async_run(*conn_, ctor_->cfg_.resolve_timeout, ctor_->cfg_.connect_timeout, token); - }, - [this](auto token) - { - return ctor_->health_checker_.async_check_health(*conn_, token); - } - ).async_wait( - asio::experimental::wait_for_one(), - std::move(self)); - - if (is_cancelled(self)) { - self.complete(asio::error::operation_aborted); - return; - } - - switch (order[0]) { - case 0: self.complete(ec1); break; - case 1: self.complete(ec2); break; - default: BOOST_ASSERT(false); - } - } - } -}; - -template -struct connect_op { - Connector* ctor_ = nullptr; - Connection* conn_ = nullptr; - asio::coroutine coro_{}; - - template - void operator()(Self& self, system::error_code ec = {}) - { - BOOST_ASIO_CORO_REENTER (coro_) for (;;) - { - BOOST_ASIO_CORO_YIELD - ctor_->async_run_check_exec(*conn_, std::move(self)); - ctor_->logger_.on_connection_lost(); - if (is_cancelled(self)) { - self.complete(asio::error::operation_aborted); - return; - } - - conn_->reset_stream(); - - if (!conn_->reconnect()) { - self.complete({}); - return; - } - - // Wait some time before trying to reconnect. - ctor_->reconnect_wait_timer_.expires_after(ctor_->cfg_.reconnect_wait_interval); - BOOST_ASIO_CORO_YIELD - ctor_->reconnect_wait_timer_.async_wait(std::move(self)); - BOOST_REDIS_CHECK_OP0(;) - } - } -}; - -template -class connector { -public: - connector(Executor ex, connect_config cfg, Logger l) - : runner_{ex, cfg.addr, l} - , health_checker_{ex, cfg.health_check_id, cfg.health_check_timeout} - , reconnect_wait_timer_{ex} - , cfg_{cfg} - , logger_{l} - { } - - template < - class Connection, - class CompletionToken = asio::default_completion_token_t - > - auto async_connect(Connection& conn, CompletionToken token = CompletionToken{}) - { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(connect_op{this, &conn}, token, conn); - } - - void cancel() - { - runner_.cancel(); - health_checker_.cancel(); - reconnect_wait_timer_.cancel(); - } - -private: - using runner_type = redis::detail::runner; - using health_checker_type = redis::detail::health_checker; - using timer_type = typename runner_type::timer_type; - - template friend struct connect_op; - template friend struct run_check_exec_op; - template friend struct run_check_op; - template friend struct hello_op; - - template - auto async_run_check(Connection& conn, CompletionToken token) - { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(run_check_op{this, &conn}, token, conn); - } - - template - auto async_run_check_exec(Connection& conn, CompletionToken token) - { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(run_check_exec_op{this, &conn}, token, conn); - } - - template - auto async_hello(Connection& conn, CompletionToken token) - { - return asio::async_compose - < CompletionToken - , void(system::error_code) - >(hello_op{this, &conn}, token, conn); - } - - void add_hello() - { - if (!cfg_.username.empty() && !cfg_.password.empty() && !cfg_.clientname.empty()) - req_hello_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password, "SETNAME", cfg_.clientname); - else if (cfg_.username.empty() && cfg_.password.empty() && cfg_.clientname.empty()) - req_hello_.push("HELLO", "3"); - else if (cfg_.clientname.empty()) - req_hello_.push("HELLO", "3", "AUTH", cfg_.username, cfg_.password); - else - req_hello_.push("HELLO", "3", "SETNAME", cfg_.clientname); - - // Subscribe to channels in the same request that sends HELLO - // because it has priority over all other requests. - // TODO: Subscribe to actual channels. - req_hello_.push("SUBSCRIBE", "channel"); - } - - runner_type runner_; - health_checker_type health_checker_; - timer_type reconnect_wait_timer_; - request req_hello_; - generic_response resp_hello_; - connect_config cfg_; - Logger logger_; -}; - -} // detail - -template < - class Socket, - class Logger = logger, - class CompletionToken = asio::default_completion_token_t -> -auto -async_connect( - basic_connection& conn, - connect_config cfg = connect_config{}, - Logger l = logger{}, - CompletionToken token = CompletionToken{}) -{ - using executor_type = typename Socket::executor_type; - using connector_type = detail::connector; - auto ctor = std::make_shared(conn.get_executor(), cfg, l); - return ctor->async_connect(conn, asio::consign(std::move(token), ctor)); -} - -} // boost::redis::experimental - -#endif // BOOST_REDIS_CONNECTOR_HPP diff --git a/include/boost/redis/impl/logger.ipp b/include/boost/redis/impl/logger.ipp new file mode 100644 index 00000000..d2c4db8c --- /dev/null +++ b/include/boost/redis/impl/logger.ipp @@ -0,0 +1,128 @@ +/* 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 + +namespace boost::redis +{ + +void logger::write_prefix() +{ + if (!std::empty(prefix_)) + std::clog << prefix_; +} + +void logger::on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res) +{ + if (level_ < level::info) + return; + + write_prefix(); + + std::clog << "Resolve results: "; + + if (ec) { + std::clog << ec.message() << std::endl; + } else { + auto begin = std::cbegin(res); + auto end = std::cend(res); + + if (begin == end) + return; + + std::clog << begin->endpoint(); + for (auto iter = std::next(begin); iter != end; ++iter) + std::clog << ", " << iter->endpoint(); + } + + std::clog << std::endl; +} + +void logger::on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep) +{ + if (level_ < level::info) + return; + + write_prefix(); + + std::clog << "Connected to endpoint: "; + + if (ec) + std::clog << ec.message() << std::endl; + else + std::clog << ep; + + std::clog << std::endl; +} + +void logger::on_ssl_handshake(system::error_code const& ec) +{ + if (level_ < level::info) + return; + + write_prefix(); + + std::clog << "SSL handshake: " << ec.message() << std::endl; +} + +void logger::on_connection_lost(system::error_code const& ec) +{ + if (level_ < level::info) + return; + + write_prefix(); + + if (ec) + std::clog << "Connection lost: " << ec.message(); + else + std::clog << "Connection lost."; + + std::clog << std::endl; +} + +void +logger::on_write( + system::error_code const& ec, + std::string const& payload) +{ + if (level_ < level::info) + return; + + write_prefix(); + + if (ec) + std::clog << "Write: " << ec.message(); + else + std::clog << "Bytes written: " << std::size(payload); + + std::clog << std::endl; +} + +void +logger::on_hello( + system::error_code const& ec, + generic_response const& resp) +{ + if (level_ < level::info) + return; + + write_prefix(); + + if (ec) { + std::clog << "Hello: " << ec.message(); + if (resp.has_error()) + std::clog << " (" << resp.error().diagnostic << ")"; + } else { + std::clog << "Hello: Success"; + } + + std::clog << std::endl; +} + +} // boost::redis diff --git a/include/boost/redis/logger.hpp b/include/boost/redis/logger.hpp index fd21256e..e3a1cd35 100644 --- a/include/boost/redis/logger.hpp +++ b/include/boost/redis/logger.hpp @@ -7,37 +7,119 @@ #ifndef BOOST_REDIS_LOGGER_HPP #define BOOST_REDIS_LOGGER_HPP +#include #include -#include -#include +#include + +namespace boost::system {class error_code;} namespace boost::redis { -// TODO: Move to ipp file. -// TODO: Implement filter. +/** @brief Logger class + * @ingroup high-level-api + * + * The class can be passed to the connection objects to log to `std::clog` + */ class logger { public: - void on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const&) + /** @brief Syslog-like log levels + * @ingroup high-level-api + */ + enum class level + { /// Emergency + emerg, + + /// Alert + alert, + + /// Critical + crit, + + /// Error + err, + + /// Warning + warning, + + /// Notice + notice, + + /// Info + info, + + /// Debug + debug + }; + + /** @brief Constructor + * @ingroup high-level-api + * + * @param l Log level. + */ + logger(level l = level::info) + : level_{l} + {} + + /** @brief Called when the resolve operation completes. + * @ingroup high-level-api + * + * @param ec Error returned by the resolve operation. + * @param res Resolve results. + */ + void on_resolve(system::error_code const& ec, asio::ip::tcp::resolver::results_type const& res); + + /** @brief Called when the connect operation completes. + * @ingroup high-level-api + * + * @param ec Error returned by the connect operation. + * @param ep Endpoint to which the connection connected. + */ + void on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const& ep); + + /** @brief Called when the ssl handshake operation completes. + * @ingroup high-level-api + * + * @param ec Error returned by the handshake operation. + */ + void on_ssl_handshake(system::error_code const& ec); + + /** @brief Called when the connection is lost. + * @ingroup high-level-api + * + * @param ec Error returned when the connection is lost. + */ + void on_connection_lost(system::error_code const& ec); + + /** @brief Called when the write operation completes. + * @ingroup high-level-api + * + * @param ec Error code returned by the write operation. + * @param payload The payload written to the socket. + */ + void on_write(system::error_code const& ec, std::string const& payload); + + /** @brief Called when the `HELLO` request completes. + * @ingroup high-level-api + * + * @param ec Error code returned by the async_exec operation. + * @param resp Response sent by the Redis server. + */ + void on_hello(system::error_code const& ec, generic_response const& resp); + + /** @brief Sets a prefix to every log message + * @ingroup high-level-api + * + * @param prefix The prefix. + */ + void set_prefix(std::string_view prefix) { - // TODO: Print the endpoints - std::clog << "on_resolve: " << ec.message() << std::endl; + prefix_ = prefix; } - void on_connect(system::error_code const& ec, asio::ip::tcp::endpoint const&) - { - // TODO: Print the endpoint - std::clog << "on_connect: " << ec.message() << std::endl; - } - - void on_connection_lost() - { - std::clog << "on_connection_lost: " << std::endl; - } - - void on_hello(system::error_code const& ec) - { - std::clog << "on_hello: " << ec.message() << std::endl; - } +private: + void write_prefix(); + level level_; + std::string_view prefix_; }; } // boost::redis diff --git a/include/boost/redis/operation.hpp b/include/boost/redis/operation.hpp index 58f3c6de..d37145c7 100644 --- a/include/boost/redis/operation.hpp +++ b/include/boost/redis/operation.hpp @@ -9,19 +9,29 @@ namespace boost::redis { -/** \brief Connection operations that can be cancelled. - * \ingroup high-level-api +/** @brief Connection operations that can be cancelled. + * @ingroup high-level-api * * The operations listed below can be passed to the * `boost::redis::connection::cancel` member function. */ enum class operation { + /// Resolve operation. + resolve, + /// Connect operation. + connect, + /// SSL handshake operation. + ssl_handshake, /// Refers to `connection::async_exec` operations. exec, /// Refers to `connection::async_run` operations. run, /// Refers to `connection::async_receive` operations. receive, + /// Cancels reconnection. + reconnection, + /// Health check operation. + health_check, /// Refers to all operations. all, }; diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index 69f269f0..08006b8a 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -12,6 +12,7 @@ #include #include +#include // NOTE: For some commands like hset it would be a good idea to assert // the value type is a pair. @@ -138,12 +139,14 @@ public: template void push(std::string_view cmd, Ts const&... args) { + auto const size_before = std::size(payload_); auto constexpr pack_size = sizeof...(Ts); resp3::add_header(payload_, resp3::type::array, 1 + pack_size); resp3::add_bulk(payload_, cmd); resp3::add_bulk(payload_, std::tie(std::forward(args)...)); + auto const size_after = std::size(payload_); - check_cmd(cmd); + check_cmd(cmd, size_after - size_before); } /** @brief Appends a new command to the end of the request. @@ -191,6 +194,7 @@ public: if (begin == end) return; + auto const size_before = std::size(payload_); auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); resp3::add_header(payload_, resp3::type::array, 2 + size * distance); @@ -200,7 +204,9 @@ public: for (; begin != end; ++begin) resp3::add_bulk(payload_, *begin); - check_cmd(cmd); + auto const size_after = std::size(payload_); + + check_cmd(cmd, size_after - size_before); } /** @brief Appends a new command to the end of the request. @@ -243,6 +249,7 @@ public: if (begin == end) return; + auto const size_before = std::size(payload_); auto constexpr size = resp3::bulk_counter::size; auto const distance = std::distance(begin, end); resp3::add_header(payload_, resp3::type::array, 1 + size * distance); @@ -251,7 +258,9 @@ public: for (; begin != end; ++begin) resp3::add_bulk(payload_, *begin); - check_cmd(cmd); + auto const size_after = std::size(payload_); + + check_cmd(cmd, size_after - size_before); } /** @brief Appends a new command to the end of the request. @@ -299,13 +308,18 @@ public: } private: - void check_cmd(std::string_view cmd) + void check_cmd(std::string_view cmd, std::size_t n) { if (!detail::has_response(cmd)) ++commands_; - if (cmd == "HELLO") + if (cmd == "HELLO") { has_hello_priority_ = cfg_.hello_with_priority; + if (has_hello_priority_) { + auto const shift = std::size(payload_) - n; + std::rotate(std::begin(payload_), std::begin(payload_) + shift, std::end(payload_)); + } + } } config cfg_; diff --git a/include/boost/redis/resp3/impl/serialization.ipp b/include/boost/redis/resp3/impl/serialization.ipp index b4efc6d2..5fcbb77f 100644 --- a/include/boost/redis/resp3/impl/serialization.ipp +++ b/include/boost/redis/resp3/impl/serialization.ipp @@ -21,7 +21,6 @@ void boost_redis_to_bulk(std::string& payload, std::string_view data) void add_header(std::string& payload, type t, std::size_t size) { - // TODO: Call reserve. auto const str = std::to_string(size); payload += to_code(t); diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index b1a8e38c..5d36db3f 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -84,7 +84,6 @@ void add_header(std::string& payload, type t, std::size_t size); template void add_bulk(std::string& payload, T const& data) { - // TODO: Call reserve. add_bulk_impl::add(payload, data); } diff --git a/include/boost/redis/run.hpp b/include/boost/redis/run.hpp deleted file mode 100644 index 5da3dabb..00000000 --- a/include/boost/redis/run.hpp +++ /dev/null @@ -1,65 +0,0 @@ -/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) - * - * Distributed under the Boost Software License, Version 1.0. (See - * accompanying file LICENSE.txt) - */ - -#ifndef BOOST_REDIS_RUN_HPP -#define BOOST_REDIS_RUN_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace boost::redis -{ - -/** @brief Call async_run on the connection. - * @ingroup high-level-api - * - * This is a facility function that - * 1. Resoves the endpoint. - * 2. Connects to one of the endpoints from 1. - * 3. Calls async_run on the underlying connection. - * - * @param conn A connection to Redis. - * @param host Redis host to connect to. - * @param port Redis port to connect to. - * @param resolve_timeout Time the resolve operation is allowed to take. - * @param connect_timeout Time the connect operation is allowed to take. - * @param token Completion token. - */ -template < - class Socket, - class Logger = logger, - class CompletionToken = asio::default_completion_token_t -> -auto -async_run( - basic_connection& conn, - address addr = address{"127.0.0.1", "6379"}, - std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10}, - std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10}, - Logger l = Logger{}, - CompletionToken token = CompletionToken{}) -{ - using executor_type = typename Socket::executor_type; - using runner_type = detail::runner; - auto runner = std::make_shared(conn.get_executor(), addr, l); - - return - runner->async_run( - conn, - resolve_timeout, - connect_timeout, - asio::consign(std::move(token), runner)); -} - -} // boost::redis - -#endif // BOOST_REDIS_RUN_HPP diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index bec18b70..ef978afc 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include diff --git a/include/boost/redis/ssl/connection.hpp b/include/boost/redis/ssl/connection.hpp index e1a895ac..37da52a8 100644 --- a/include/boost/redis/ssl/connection.hpp +++ b/include/boost/redis/ssl/connection.hpp @@ -8,7 +8,11 @@ #define BOOST_REDIS_SSL_CONNECTION_HPP #include +#include +#include +#include #include +#include #include #include @@ -56,7 +60,10 @@ public: explicit basic_connection(executor_type ex, asio::ssl::context& ctx) : base_type{ex} - , stream_{ex, ctx} + , ctx_{&ctx} + , reconn_{ex} + , runner_{ex, {}} + , stream_{std::make_unique(ex, ctx)} { } /// Constructor @@ -66,28 +73,40 @@ public: { } /// Returns the associated executor. - auto get_executor() {return stream_.get_executor();} + auto get_executor() {return stream_->get_executor();} /// Reset the underlying stream. - void reset_stream(asio::ssl::context& ctx) + void reset_stream() { - stream_ = next_layer_type{stream_.get_executor(), ctx}; + if (stream_->next_layer().is_open()) { + stream_->next_layer().close(); + stream_ = std::make_unique(stream_->get_executor(), *ctx_); + } } /// Returns a reference to the next layer. - auto& next_layer() noexcept { return stream_; } + auto& next_layer() noexcept { return *stream_; } /// Returns a const reference to the next layer. - auto const& next_layer() const noexcept { return stream_; } + auto const& next_layer() const noexcept { return *stream_; } /** @brief Establishes a connection with the Redis server asynchronously. * * See redis::connection::async_run for more information. */ - template > - auto async_run(CompletionToken token = CompletionToken{}) + template < + class Logger = logger, + class CompletionToken = asio::default_completion_token_t> + auto + async_run( + config const& cfg = {}, + Logger l = Logger{}, + CompletionToken token = CompletionToken{}) { - return base_type::async_run(std::move(token)); + reconn_.set_wait_interval(cfg.reconnect_wait_interval); + runner_.set_config(cfg); + l.set_prefix(runner_.get_config().log_prefix); + return reconn_.async_run(*this, l, std::move(token)); } /** @brief Executes a command on the Redis server asynchronously. @@ -124,9 +143,13 @@ public: * See redis::connection::cancel for more information. */ auto cancel(operation op = operation::all) -> std::size_t - { return base_type::cancel(op); } + { + reconn_.cancel(op); + runner_.cancel(op); + return base_type::cancel(op); + } - auto& lowest_layer() noexcept { return stream_.lowest_layer(); } + auto& lowest_layer() noexcept { return stream_->lowest_layer(); } /// Sets the maximum size of the read buffer. void set_max_buffer_read_size(std::size_t max_read_size) noexcept @@ -143,22 +166,43 @@ public: void reserve(std::size_t read, std::size_t write) { base_type::reserve(read, write); } + /// Returns true if the connection was canceled. + bool is_cancelled() const noexcept + { return reconn_.is_cancelled();} + private: + using runner_type = redis::detail::runner; + using reconnection_type = redis::detail::basic_reconnection; using this_type = basic_connection; + template + auto async_run_one(Logger l, CompletionToken token) + { return runner_.async_run(*this, l, std::move(token)); } + + template + auto async_run_impl(Logger l, CompletionToken token) + { return base_type::async_run_impl(l, std::move(token)); } + template friend class redis::detail::connection_base; + template friend class redis::detail::read_next_op; template friend struct redis::detail::exec_op; - template friend struct redis::detail::exec_read_op; - template friend struct detail::receive_op; - template friend struct redis::detail::run_op; - template friend struct redis::detail::writer_op; + template friend struct redis::detail::receive_op; + template friend struct redis::detail::run_op; + template friend struct redis::detail::writer_op; template friend struct redis::detail::reader_op; - template friend struct detail::wait_receive_op; + template friend struct redis::detail::wait_receive_op; + template friend struct redis::detail::run_all_op; + template friend struct redis::detail::reconnection_op; - auto is_open() const noexcept { return stream_.next_layer().is_open(); } - void close() { stream_.next_layer().close(); } + auto is_open() const noexcept { return stream_->next_layer().is_open(); } - next_layer_type stream_; + void close() + { reset_stream(); } + + asio::ssl::context* ctx_; + reconnection_type reconn_; + runner_type runner_; + std::unique_ptr stream_; }; /** \brief A connection that uses a boost::asio::ssl::stream. diff --git a/include/boost/redis/ssl/detail/handshaker.hpp b/include/boost/redis/ssl/detail/handshaker.hpp new file mode 100644 index 00000000..987c6ef3 --- /dev/null +++ b/include/boost/redis/ssl/detail/handshaker.hpp @@ -0,0 +1,124 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_SSL_CONNECTOR_HPP +#define BOOST_REDIS_SSL_CONNECTOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::redis::ssl::detail +{ + +template +struct handshake_op { + Handshaker* hsher_ = nullptr; + Stream* stream_ = nullptr; + asio::coroutine coro{}; + + template + void operator()( Self& self + , std::array const& order = {} + , system::error_code const& ec1 = {} + , system::error_code const& ec2 = {}) + { + BOOST_ASIO_CORO_REENTER (coro) + { + hsher_->timer_.expires_after(hsher_->timeout_); + + BOOST_ASIO_CORO_YIELD + asio::experimental::make_parallel_group( + [this](auto token) { return stream_->async_handshake(asio::ssl::stream_base::client, token); }, + [this](auto token) { return hsher_->timer_.async_wait(token);} + ).async_wait( + asio::experimental::wait_for_one(), + std::move(self)); + + if (is_cancelled(self)) { + self.complete(asio::error::operation_aborted); + return; + } + + switch (order[0]) { + case 0: { + self.complete(ec1); + } break; + case 1: + { + if (ec2) { + self.complete(ec2); + } else { + self.complete(error::ssl_handshake_timeout); + } + } break; + + default: BOOST_ASSERT(false); + } + } + } +}; + +template +class handshaker { +public: + using timer_type = + asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + Executor>; + + handshaker(Executor ex) + : timer_{ex} + {} + + template + auto + async_handshake(Stream& stream, CompletionToken&& token) + { + return asio::async_compose + < CompletionToken + , void(system::error_code) + >(handshake_op{this, &stream}, token, timer_); + } + + std::size_t cancel(operation op) + { + switch (op) { + case operation::ssl_handshake: + case operation::all: + timer_.cancel(); + break; + default: /* ignore */; + } + + return 0; + } + + constexpr bool is_dummy() const noexcept + {return false;} + + void set_config(config const& cfg) + { timeout_ = cfg.ssl_handshake_timeout; } + +private: + template friend struct handshake_op; + + timer_type timer_; + std::chrono::steady_clock::duration timeout_; +}; + +} // boost::redis::ssl::detail + +#endif // BOOST_REDIS_SSL_CONNECTOR_HPP diff --git a/tests/common.cpp b/tests/common.cpp new file mode 100644 index 00000000..6583ffd5 --- /dev/null +++ b/tests/common.cpp @@ -0,0 +1,29 @@ +#include "common.hpp" +#include +#include + +#include + +struct run_callback { + std::shared_ptr conn; + boost::redis::operation op; + boost::system::error_code expected; + + void operator()(boost::system::error_code const& ec) const + { + std::cout << "async_run: " << ec.message() << std::endl; + //BOOST_CHECK_EQUAL(ec, expected); + conn->cancel(op); + } +}; + +void +run( + std::shared_ptr conn, + boost::redis::config cfg, + boost::system::error_code ec, + boost::redis::operation op) +{ + conn->async_run(cfg, {}, run_callback{conn, op, ec}); +} + diff --git a/tests/common.hpp b/tests/common.hpp index c7d1e5c3..3f8909d2 100644 --- a/tests/common.hpp +++ b/tests/common.hpp @@ -3,10 +3,23 @@ #include #include #include +#include +#include +#include + +namespace net = boost::asio; #ifdef BOOST_ASIO_HAS_CO_AWAIT -namespace net = boost::asio; + inline auto redir(boost::system::error_code& ec) - { return net::redirect_error(net::use_awaitable, ec); } + { return net::redirect_error(boost::asio::use_awaitable, ec); } #endif // BOOST_ASIO_HAS_CO_AWAIT + +void +run( + std::shared_ptr conn, + boost::redis::config cfg = {}, + boost::system::error_code ec = boost::asio::error::operation_aborted, + boost::redis::operation op = boost::redis::operation::receive); + diff --git a/tests/conn_check_health.cpp b/tests/conn_check_health.cpp index 87d26949..34a22496 100644 --- a/tests/conn_check_health.cpp +++ b/tests/conn_check_health.cpp @@ -4,9 +4,7 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include +#include #include #define BOOST_TEST_MODULE check-health #include @@ -15,58 +13,59 @@ #include namespace net = boost::asio; +namespace redis = boost::redis; using error_code = boost::system::error_code; using connection = boost::redis::connection; using boost::redis::request; using boost::redis::ignore; using boost::redis::operation; using boost::redis::generic_response; -using boost::redis::async_check_health; -using boost::redis::async_run; using boost::redis::logger; -using boost::redis::address; -using namespace std::chrono_literals; +using redis::config; + +// TODO: Test cancel(health_check) std::chrono::seconds const interval{1}; struct push_callback { - connection* conn; + connection* conn1; connection* conn2; - generic_response* resp; - request* req; + generic_response* resp2; + request* req1; int i = 0; + boost::asio::coroutine coro{}; void operator()(error_code ec = {}, std::size_t = 0) { - ++i; - if (ec) { - std::clog << "Exiting." << std::endl; - return; - } + BOOST_ASIO_CORO_REENTER (coro) for (;;) + { + resp2->value().clear(); + BOOST_ASIO_CORO_YIELD + conn2->async_receive(*resp2, *this); + if (ec) { + std::clog << "Exiting." << std::endl; + return; + } - if (resp->value().empty()) { - // First call - BOOST_TEST(!ec); - conn2->async_receive(*resp, *this); - } else if (i == 5) { - std::clog << "Pausing the server" << std::endl; - // Pause the redis server to test if the health-check exits. - conn->async_exec(*req, ignore, [](auto ec, auto) { - std::clog << "Pausing callback> " << ec.message() << std::endl; + BOOST_TEST(resp2->has_value()); + BOOST_TEST(!resp2->value().empty()); + std::clog << "Event> " << resp2->value().front().value << std::endl; + + ++i; + + if (i == 5) { + std::clog << "Pausing the server" << std::endl; + // Pause the redis server to test if the health-check exits. + BOOST_ASIO_CORO_YIELD + conn1->async_exec(*req1, ignore, *this); + std::clog << "After pausing> " << ec.message() << std::endl; // Don't know in CI we are getting: Got RESP3 simple-error. //BOOST_TEST(!ec); - }); - conn2->cancel(operation::run); - conn2->cancel(operation::receive); - } else { - BOOST_TEST(!ec); - // Expect 3 pongs and pause the clients so check-health exists - // without error. - BOOST_TEST(resp->has_value()); - BOOST_TEST(!resp->value().empty()); - std::clog << "Event> " << resp->value().front().value << std::endl; - resp->value().clear(); - conn2->async_receive(*resp, *this); + conn2->cancel(operation::run); + conn2->cancel(operation::receive); + conn2->cancel(operation::reconnection); + return; + } } }; }; @@ -75,48 +74,51 @@ BOOST_AUTO_TEST_CASE(check_health) { net::io_context ioc; - connection conn{ioc}; + + connection conn1{ioc}; + conn1.cancel(operation::reconnection); + + request req1; + req1.push("CLIENT", "PAUSE", "10000", "ALL"); + + config cfg1; + cfg1.health_check_id = "conn1"; + error_code res1; + conn1.async_run(cfg1, {}, [&](auto ec) { + std::cout << "async_run 1 completed: " << ec.message() << std::endl; + res1 = ec; + }); + + //-------------------------------- // It looks like client pause does not work for clients that are // sending MONITOR. I will therefore open a second connection. connection conn2{ioc}; - std::string const msg = "test-check-health"; - - bool seen = false; - async_check_health(conn, msg, interval, [&](auto ec) { - BOOST_CHECK_EQUAL(ec, boost::redis::error::pong_timeout); - std::cout << "async_check_health: completed: " << ec.message() << std::endl; - seen = true; - }); - - request req; - req.push("HELLO", 3); - req.push("MONITOR"); - - conn2.async_exec(req, ignore, [](auto ec, auto) { - std::cout << "A" << std::endl; - BOOST_TEST(!ec); + config cfg2; + cfg2.health_check_id = "conn2"; + error_code res2; + conn2.async_run(cfg2, {}, [&](auto ec){ + std::cout << "async_run 2 completed: " << ec.message() << std::endl; + res2 = ec; }); request req2; - req2.push("HELLO", "3"); - req2.push("CLIENT", "PAUSE", "5000", "ALL"); + req2.push("MONITOR"); + generic_response resp2; - generic_response resp; - push_callback{&conn, &conn2, &resp, &req2}(); // Starts reading pushes. - - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - std::cout << "B" << std::endl; - BOOST_TEST(!!ec); + conn2.async_exec(req2, ignore, [](auto ec, auto) { + std::cout << "async_exec: " << std::endl; + BOOST_TEST(!ec); }); - async_run(conn2, address{}, 10s, 10s, logger{}, [](auto ec){ - std::cout << "C" << std::endl; - BOOST_TEST(!!ec); - }); + //-------------------------------- + + push_callback{&conn1, &conn2, &resp2, &req1}(); // Starts reading pushes. ioc.run(); - BOOST_TEST(seen); + + BOOST_TEST(!!res1); + BOOST_TEST(!!res2); } diff --git a/tests/conn_echo_stress.cpp b/tests/conn_echo_stress.cpp index 8b815837..3adb1a0f 100644 --- a/tests/conn_echo_stress.cpp +++ b/tests/conn_echo_stress.cpp @@ -4,7 +4,7 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include #include @@ -24,10 +24,9 @@ using boost::redis::request; using boost::redis::response; using boost::redis::ignore; using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::address; -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; -using namespace std::chrono_literals; +using boost::redis::logger; +using boost::redis::config; +using boost::redis::connection; auto push_consumer(std::shared_ptr conn, int expected) -> net::awaitable { @@ -38,10 +37,7 @@ auto push_consumer(std::shared_ptr conn, int expected) -> net::await break; } - request req; - req.push("HELLO", 3); - req.push("QUIT"); - co_await conn->async_exec(req, ignore); + conn->cancel(); } auto echo_session(std::shared_ptr conn, std::string id, int n) -> net::awaitable @@ -81,8 +77,7 @@ auto async_echo_stress() -> net::awaitable net::co_spawn(ex, echo_session(conn, std::to_string(i), msgs), net::detached); - address addr; - co_await async_run(*conn, addr); + run(conn); } BOOST_AUTO_TEST_CASE(echo_stress) diff --git a/tests/conn_exec.cpp b/tests/conn_exec.cpp index a4dcfffc..c9eaeac2 100644 --- a/tests/conn_exec.cpp +++ b/tests/conn_exec.cpp @@ -4,9 +4,7 @@ * accompanying file LICENSE.txt) */ -#include -#include -#include +#include #include #define BOOST_TEST_MODULE conn-exec #include @@ -20,17 +18,14 @@ // container. namespace net = boost::asio; -using error_code = boost::system::error_code; -using connection = boost::redis::connection; +using boost::redis::connection; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; -using boost::redis::ignore_t; -using boost::redis::async_run; -using boost::redis::logger; -using boost::redis::address; -using namespace std::chrono_literals; +using boost::redis::operation; +// Sends three requests where one of them has a hello with a priority +// set, which means it should be executed first. BOOST_AUTO_TEST_CASE(hello_priority) { request req1; @@ -40,7 +35,6 @@ BOOST_AUTO_TEST_CASE(hello_priority) req2.get_config().hello_with_priority = false; req2.push("HELLO", 3); req2.push("PING", "req2"); - req2.push("QUIT"); request req3; req3.get_config().hello_with_priority = true; @@ -49,60 +43,63 @@ BOOST_AUTO_TEST_CASE(hello_priority) net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); bool seen1 = false; bool seen2 = false; bool seen3 = false; - conn.async_exec(req1, ignore, [&](auto ec, auto){ - std::cout << "bbb" << std::endl; + conn->async_exec(req1, ignore, [&](auto ec, auto){ + // Second callback to the called. + std::cout << "req1" << std::endl; BOOST_TEST(!ec); BOOST_TEST(!seen2); BOOST_TEST(seen3); seen1 = true; }); - conn.async_exec(req2, ignore, [&](auto ec, auto){ - std::cout << "ccc" << std::endl; + + conn->async_exec(req2, ignore, [&](auto ec, auto){ + // Last callback to the called. + std::cout << "req2" << std::endl; BOOST_TEST(!ec); BOOST_TEST(seen1); BOOST_TEST(seen3); seen2 = true; + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); - conn.async_exec(req3, ignore, [&](auto ec, auto){ - std::cout << "ddd" << std::endl; + + conn->async_exec(req3, ignore, [&](auto ec, auto){ + // Callback that will be called first. + std::cout << "req3" << std::endl; BOOST_TEST(!ec); BOOST_TEST(!seen1); BOOST_TEST(!seen2); seen3 = true; }); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_TEST(!ec); - }); - + run(conn); ioc.run(); } +// Tries to receive a string in an int and gets an error. BOOST_AUTO_TEST_CASE(wrong_response_data_type) { request req; - req.push("HELLO", 3); - req.push("QUIT"); + req.push("PING"); // Wrong data type. - response resp; + response resp; net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); - conn.async_exec(req, resp, [](auto ec, auto){ + conn->async_exec(req, resp, [conn](auto ec, auto){ BOOST_CHECK_EQUAL(ec, boost::redis::error::not_a_number); - }); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); + conn->cancel(operation::reconnection); }); + run(conn); ioc.run(); } @@ -110,13 +107,13 @@ BOOST_AUTO_TEST_CASE(cancel_request_if_not_connected) { request req; req.get_config().cancel_if_not_connected = true; - req.push("HELLO", 3); req.push("PING"); net::io_context ioc; auto conn = std::make_shared(ioc); - conn->async_exec(req, ignore, [](auto ec, auto){ + conn->async_exec(req, ignore, [conn](auto ec, auto){ BOOST_CHECK_EQUAL(ec, boost::redis::error::not_connected); + conn->cancel(); }); ioc.run(); diff --git a/tests/conn_exec_cancel.cpp b/tests/conn_exec_cancel.cpp index 3425b438..882698f4 100644 --- a/tests/conn_exec_cancel.cpp +++ b/tests/conn_exec_cancel.cpp @@ -4,8 +4,7 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include #include #define BOOST_TEST_MODULE conn-exec-cancel #include @@ -17,89 +16,37 @@ #ifdef BOOST_ASIO_HAS_CO_AWAIT #include -// NOTE1: Sends hello separately. I have observed that if hello and +// NOTE1: I have observed that if hello and // blpop are sent toguether, Redis will send the response of hello -// right away, not waiting for blpop. That is why we have to send it -// separately here. +// right away, not waiting for blpop. namespace net = boost::asio; using error_code = boost::system::error_code; using namespace net::experimental::awaitable_operators; using boost::redis::operation; +using boost::redis::error; using boost::redis::request; using boost::redis::response; using boost::redis::generic_response; using boost::redis::ignore; using boost::redis::ignore_t; -using boost::redis::async_run; +using boost::redis::config; using boost::redis::logger; -using boost::redis::address; -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; +using boost::redis::connection; using namespace std::chrono_literals; -auto async_ignore_explicit_cancel_of_req_written() -> net::awaitable -{ - auto ex = co_await net::this_coro::executor; - - generic_response gresp; - auto conn = std::make_shared(ex); - - async_run(*conn, address{}, 10s, 10s, logger{}, [conn](auto ec) { - std::cout << "async_run: " << ec.message() << std::endl; - BOOST_TEST(!ec); - }); - - net::steady_timer st{ex}; - st.expires_after(std::chrono::seconds{1}); - - // See NOTE1. - request req0; - req0.push("HELLO", 3); - co_await conn->async_exec(req0, gresp, net::use_awaitable); - - request req1; - req1.push("BLPOP", "any", 3); - - // Should not be canceled. - bool seen = false; - conn->async_exec(req1, gresp, [&](auto ec, auto) mutable{ - std::cout << "async_exec (1): " << ec.message() << std::endl; - BOOST_TEST(!ec); - seen = true; - }); - - // Will complete while BLPOP is pending. - boost::system::error_code ec1; - co_await st.async_wait(net::redirect_error(net::use_awaitable, ec1)); - conn->cancel(operation::exec); - - BOOST_TEST(!ec1); - - request req3; - req3.push("QUIT"); - - // Test whether the connection remains usable after a call to - // cancel(exec). - co_await conn->async_exec(req3, gresp, net::redirect_error(net::use_awaitable, ec1)); - - BOOST_TEST(!ec1); - BOOST_TEST(seen); -} - -auto ignore_implicit_cancel_of_req_written() -> net::awaitable +auto implicit_cancel_of_req_written() -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - // Calls async_run separately from the group of ops below to avoid - // having it canceled when the timer fires. - async_run(*conn, address{}, 10s, 10s, logger{}, [conn](auto ec) { - BOOST_CHECK_EQUAL(ec, net::error::basic_errors::operation_aborted); - }); + config cfg; + cfg.health_check_interval = std::chrono::seconds{0}; + run(conn, cfg); // See NOTE1. request req0; - req0.push("HELLO", 3); + req0.push("PING"); co_await conn->async_exec(req0, ignore, net::use_awaitable); // Will be cancelled after it has been written but before the @@ -110,33 +57,33 @@ auto ignore_implicit_cancel_of_req_written() -> net::awaitable net::steady_timer st{ex}; st.expires_after(std::chrono::seconds{1}); + // Achieves implicit cancellation when the timer fires. boost::system::error_code ec1, ec2; co_await ( conn->async_exec(req1, ignore, redir(ec1)) || st.async_wait(redir(ec2)) ); - BOOST_CHECK_EQUAL(ec1, net::error::basic_errors::operation_aborted); - BOOST_TEST(!ec2); -} + conn->cancel(); -BOOST_AUTO_TEST_CASE(test_ignore_explicit_cancel_of_req_written) -{ - start(async_ignore_explicit_cancel_of_req_written()); + // I have observed this produces terminal cancellation so it can't + // be ignored, an error is expected. + BOOST_CHECK_EQUAL(ec1, net::error::operation_aborted); + BOOST_TEST(!ec2); } BOOST_AUTO_TEST_CASE(test_ignore_implicit_cancel_of_req_written) { - start(ignore_implicit_cancel_of_req_written()); + start(implicit_cancel_of_req_written()); } BOOST_AUTO_TEST_CASE(test_cancel_of_req_written_on_run_canceled) { net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); request req0; - req0.push("HELLO", 3); + req0.push("PING"); // Sends a request that will be blocked forever, so we can test // canceling it while waiting for a response. @@ -147,26 +94,27 @@ BOOST_AUTO_TEST_CASE(test_cancel_of_req_written_on_run_canceled) auto c1 = [&](auto ec, auto) { - BOOST_CHECK_EQUAL(ec, net::error::basic_errors::operation_aborted); + BOOST_CHECK_EQUAL(ec, net::error::operation_aborted); }; auto c0 = [&](auto ec, auto) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, net::error::basic_errors::operation_aborted); - }); + config cfg; + cfg.health_check_interval = std::chrono::seconds{5}; + run(conn); net::steady_timer st{ioc}; st.expires_after(std::chrono::seconds{1}); st.async_wait([&](auto ec){ BOOST_TEST(!ec); - conn.cancel(operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); ioc.run(); diff --git a/tests/conn_exec_cancel2.cpp b/tests/conn_exec_cancel2.cpp new file mode 100644 index 00000000..83b383c4 --- /dev/null +++ b/tests/conn_exec_cancel2.cpp @@ -0,0 +1,96 @@ +/* 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 +#define BOOST_TEST_MODULE conn-exec-cancel +#include +#include "common.hpp" +#include "../examples/start.hpp" +#include +#include + +#ifdef BOOST_ASIO_HAS_CO_AWAIT +#include + +// NOTE1: Sends hello separately. I have observed that if hello and +// blpop are sent toguether, Redis will send the response of hello +// right away, not waiting for blpop. That is why we have to send it +// separately. + +namespace net = boost::asio; +using error_code = boost::system::error_code; +using namespace net::experimental::awaitable_operators; +using boost::redis::operation; +using boost::redis::request; +using boost::redis::response; +using boost::redis::generic_response; +using boost::redis::ignore; +using boost::redis::ignore_t; +using boost::redis::config; +using boost::redis::logger; +using boost::redis::connection; +using namespace std::chrono_literals; + +auto async_ignore_explicit_cancel_of_req_written() -> net::awaitable +{ + auto ex = co_await net::this_coro::executor; + + generic_response gresp; + auto conn = std::make_shared(ex); + + run(conn); + + net::steady_timer st{ex}; + st.expires_after(std::chrono::seconds{1}); + + // See NOTE1. + request req0; + req0.push("PING", "async_ignore_explicit_cancel_of_req_written"); + co_await conn->async_exec(req0, gresp, net::use_awaitable); + + request req1; + req1.push("BLPOP", "any", 3); + + bool seen = false; + conn->async_exec(req1, gresp, [&](auto ec, auto) mutable{ + // No error should occur since the cancelation should be + // ignored. + std::cout << "async_exec (1): " << ec.message() << std::endl; + BOOST_TEST(!ec); + seen = true; + }); + + // Will complete while BLPOP is pending. + boost::system::error_code ec1; + co_await st.async_wait(net::redirect_error(net::use_awaitable, ec1)); + conn->cancel(operation::exec); + + BOOST_TEST(!ec1); + + request req3; + req3.push("PING"); + + // Test whether the connection remains usable after a call to + // cancel(exec). + co_await conn->async_exec(req3, gresp, net::redirect_error(net::use_awaitable, ec1)); + conn->cancel(); + + BOOST_TEST(!ec1); + BOOST_TEST(seen); +} + +BOOST_AUTO_TEST_CASE(test_ignore_explicit_cancel_of_req_written) +{ + start(async_ignore_explicit_cancel_of_req_written()); +} + +#else +BOOST_AUTO_TEST_CASE(dummy) +{ + BOOST_TEST(true); +} +#endif diff --git a/tests/conn_exec_error.cpp b/tests/conn_exec_error.cpp index 93ae5bff..45b9de2c 100644 --- a/tests/conn_exec_error.cpp +++ b/tests/conn_exec_error.cpp @@ -4,7 +4,7 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include #define BOOST_TEST_MODULE conn-exec-error @@ -24,9 +24,9 @@ using boost::redis::generic_response; using boost::redis::ignore; using boost::redis::ignore_t; using boost::redis::error; -using boost::redis::async_run; using boost::redis::logger; -using boost::redis::address; +using boost::redis::operation; +using redis::config; using namespace std::chrono_literals; BOOST_AUTO_TEST_CASE(no_ignore_error) @@ -38,16 +38,16 @@ BOOST_AUTO_TEST_CASE(no_ignore_error) net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); - conn.async_exec(req, ignore, [&](auto ec, auto){ + conn->async_exec(req, ignore, [&](auto ec, auto){ BOOST_CHECK_EQUAL(ec, error::resp3_simple_error); - conn.cancel(redis::operation::run); - }); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); + run(conn); + ioc.run(); } @@ -64,10 +64,10 @@ BOOST_AUTO_TEST_CASE(has_diagnostic) net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); response resp; - conn.async_exec(req, resp, [&](auto ec, auto){ + conn->async_exec(req, resp, [&](auto ec, auto){ BOOST_TEST(!ec); // HELLO @@ -81,12 +81,12 @@ BOOST_AUTO_TEST_CASE(has_diagnostic) BOOST_TEST(std::get<1>(resp).has_value()); BOOST_CHECK_EQUAL(std::get<1>(resp).value(), "Barra do Una"); - conn.cancel(redis::operation::run); - }); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); + run(conn); + ioc.run(); } @@ -106,14 +106,15 @@ BOOST_AUTO_TEST_CASE(resp3_error_in_cmd_pipeline) response resp2; net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); auto c2 = [&](auto ec, auto) { BOOST_TEST(!ec); BOOST_TEST(std::get<0>(resp2).has_value()); BOOST_CHECK_EQUAL(std::get<0>(resp2).value(), "req2-msg1"); - conn.cancel(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }; auto c1 = [&](auto ec, auto) @@ -128,13 +129,11 @@ BOOST_AUTO_TEST_CASE(resp3_error_in_cmd_pipeline) BOOST_TEST(std::get<3>(resp1).has_value()); BOOST_CHECK_EQUAL(std::get<3>(resp1).value(), "req1-msg3"); - conn.async_exec(req2, resp2, c2); + conn->async_exec(req2, resp2, c2); }; - conn.async_exec(req1, resp1, c1); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + conn->async_exec(req1, resp1, c1); + run(conn); ioc.run(); } @@ -163,9 +162,9 @@ BOOST_AUTO_TEST_CASE(error_in_transaction) net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); - conn.async_exec(req, resp, [&](auto ec, auto){ + conn->async_exec(req, resp, [&](auto ec, auto){ BOOST_TEST(!ec); BOOST_TEST(std::get<0>(resp).has_value()); @@ -193,12 +192,12 @@ BOOST_AUTO_TEST_CASE(error_in_transaction) BOOST_TEST(std::get<6>(resp).has_value()); BOOST_CHECK_EQUAL(std::get<6>(resp).value(), "PONG"); - conn.cancel(redis::operation::run); - }); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }); + run(conn); + ioc.run(); } @@ -215,7 +214,7 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax) req2.push("SUBSCRIBE"); // Wrong command synthax. net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); auto c2 = [&](auto ec, auto) { @@ -227,10 +226,10 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax) { std::cout << "async_exec: hello" << std::endl; BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); generic_response gresp; auto c3 = [&](auto ec, auto) @@ -241,14 +240,13 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax) BOOST_CHECK_EQUAL(gresp.error().data_type, resp3::type::simple_error); BOOST_TEST(!std::empty(gresp.error().diagnostic)); std::cout << gresp.error().diagnostic << std::endl; - conn.cancel(redis::operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); }; - conn.async_receive(gresp, c3); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - std::cout << "async_run" << std::endl; - BOOST_CHECK_EQUAL(ec, boost::asio::error::basic_errors::operation_aborted); - }); + conn->async_receive(gresp, c3); + + run(conn); ioc.run(); } diff --git a/tests/conn_exec_retry.cpp b/tests/conn_exec_retry.cpp index bf70475b..41ca94cb 100644 --- a/tests/conn_exec_retry.cpp +++ b/tests/conn_exec_retry.cpp @@ -4,19 +4,17 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include #define BOOST_TEST_MODULE conn-exec-retry #include - -#include -#include -#include - +#include #include "common.hpp" +#include + namespace net = boost::asio; using error_code = boost::system::error_code; using connection = boost::redis::connection; @@ -24,9 +22,8 @@ using boost::redis::operation; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; -using boost::redis::async_run; using boost::redis::logger; -using boost::redis::address; +using boost::redis::config; using namespace std::chrono_literals; BOOST_AUTO_TEST_CASE(request_retry_false) @@ -45,7 +42,7 @@ BOOST_AUTO_TEST_CASE(request_retry_false) req2.push("PING"); net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); net::steady_timer st{ioc}; st.expires_after(std::chrono::seconds{1}); @@ -55,28 +52,33 @@ BOOST_AUTO_TEST_CASE(request_retry_false) // although it has cancel_on_connection_lost = false. The reason // being it has already been written so // cancel_on_connection_lost does not apply. - conn.cancel(operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); + std::cout << "async_wait" << std::endl; }); auto c2 = [&](auto ec, auto){ + std::cout << "c2" << std::endl; BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); }; auto c1 = [&](auto ec, auto){ + std::cout << "c1" << std::endl; BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); }; auto c0 = [&](auto ec, auto){ + std::cout << "c0" << std::endl; BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req1, ignore, c1); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); - }); + config cfg; + cfg.health_check_interval = 5s; + run(conn); ioc.run(); } @@ -102,25 +104,27 @@ BOOST_AUTO_TEST_CASE(request_retry_true) req3.push("QUIT"); net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); net::steady_timer st{ioc}; st.expires_after(std::chrono::seconds{1}); st.async_wait([&](auto){ // Cancels the request before receiving the response. This // should cause the thrid request to not complete with error - // since it has cancel_if_unresponded = true and cancellation commes - // after it was written. - conn.cancel(boost::redis::operation::run); + // since it has cancel_if_unresponded = true and cancellation + // comes after it was written. + conn->cancel(operation::run); }); auto c3 = [&](auto ec, auto){ + std::cout << "c3: " << ec.message() << std::endl; BOOST_TEST(!ec); + conn->cancel(); }; auto c2 = [&](auto ec, auto){ BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c3); + conn->async_exec(req3, ignore, c3); }; auto c1 = [](auto ec, auto){ @@ -129,22 +133,17 @@ BOOST_AUTO_TEST_CASE(request_retry_true) auto c0 = [&](auto ec, auto){ BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req1, ignore, c1); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - // The first cacellation. - BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); - conn.reset_stream(); - - // Reconnects and runs again to test req3. - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - std::cout << ec.message() << std::endl; - BOOST_TEST(!ec); - }); + config cfg; + cfg.health_check_interval = 5s; + conn->async_run(cfg, {}, [&](auto ec){ + std::cout << ec.message() << std::endl; + BOOST_TEST(!!ec); }); ioc.run(); diff --git a/tests/conn_push.cpp b/tests/conn_push.cpp index e6d07333..d2a9399b 100644 --- a/tests/conn_push.cpp +++ b/tests/conn_push.cpp @@ -4,7 +4,7 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include #include @@ -17,6 +17,7 @@ #include namespace net = boost::asio; +namespace redis = boost::redis; using boost::redis::operation; using connection = boost::redis::connection; @@ -26,9 +27,8 @@ using boost::redis::request; using boost::redis::response; using boost::redis::ignore; using boost::redis::ignore_t; -using boost::redis::async_run; +using redis::config; using boost::redis::logger; -using boost::redis::address; using namespace std::chrono_literals; BOOST_AUTO_TEST_CASE(receives_push_waiting_resps) @@ -46,37 +46,35 @@ BOOST_AUTO_TEST_CASE(receives_push_waiting_resps) net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); auto c3 =[](auto ec, auto...) { BOOST_TEST(!!ec); }; - auto c2 =[&](auto ec, auto...) + auto c2 =[&, conn](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c3); + conn->async_exec(req3, ignore, c3); }; - auto c1 =[&](auto ec, auto...) + auto c1 =[&, conn](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req2, ignore, c2); }; - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - BOOST_TEST(!ec); - conn.cancel(operation::receive); - }); + run(conn, {}, {}); bool push_received = false; - conn.async_receive(ignore, [&](auto ec, auto){ + conn->async_receive(ignore, [&, conn](auto ec, auto){ std::cout << "async_receive" << std::endl; BOOST_TEST(!ec); - conn.cancel(operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); push_received = true; }); @@ -88,27 +86,25 @@ BOOST_AUTO_TEST_CASE(receives_push_waiting_resps) BOOST_AUTO_TEST_CASE(push_received1) { net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); request req; - req.push("HELLO", 3); + //req.push("HELLO", 3); req.push("SUBSCRIBE", "channel"); - conn.async_exec(req, ignore, [](auto ec, auto){ + conn->async_exec(req, ignore, [conn](auto ec, auto){ std::cout << "async_exec" << std::endl; BOOST_TEST(!ec); }); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - std::cout << "async_run: " << ec.message() << std::endl; - conn.cancel(operation::receive); - }); + run(conn); bool push_received = false; - conn.async_receive(ignore, [&](auto ec, auto){ + conn->async_receive(ignore, [&, conn](auto ec, auto){ std::cout << "async_receive" << std::endl; BOOST_TEST(!ec); - conn.cancel(operation::run); + conn->cancel(operation::run); + conn->cancel(operation::reconnection); push_received = true; }); @@ -120,7 +116,7 @@ BOOST_AUTO_TEST_CASE(push_received1) BOOST_AUTO_TEST_CASE(push_filtered_out) { net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); request req; req.push("HELLO", 3); @@ -129,17 +125,16 @@ BOOST_AUTO_TEST_CASE(push_filtered_out) req.push("QUIT"); response resp; - conn.async_exec(req, resp, [](auto ec, auto){ + conn->async_exec(req, resp, [conn](auto ec, auto){ BOOST_TEST(!ec); }); - conn.async_receive(ignore, [](auto ec, auto){ + conn->async_receive(ignore, [conn](auto ec, auto){ BOOST_TEST(!ec); + conn->cancel(operation::reconnection); }); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - BOOST_TEST(!ec); - }); + run(conn); ioc.run(); @@ -148,15 +143,16 @@ BOOST_AUTO_TEST_CASE(push_filtered_out) } #ifdef BOOST_ASIO_HAS_CO_AWAIT -net::awaitable push_consumer1(connection& conn, bool& push_received) +net::awaitable +push_consumer1(std::shared_ptr conn, bool& push_received) { { - auto [ec, ev] = co_await conn.async_receive(ignore, as_tuple(net::use_awaitable)); + auto [ec, ev] = co_await conn->async_receive(ignore, as_tuple(net::use_awaitable)); BOOST_TEST(!ec); } { - auto [ec, ev] = co_await conn.async_receive(ignore, as_tuple(net::use_awaitable)); + auto [ec, ev] = co_await conn->async_receive(ignore, as_tuple(net::use_awaitable)); BOOST_CHECK_EQUAL(ec, net::experimental::channel_errc::channel_cancelled); } @@ -187,7 +183,7 @@ auto boost_redis_adapt(response_error_tag&) BOOST_AUTO_TEST_CASE(test_push_adapter) { net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); request req; req.push("HELLO", 3); @@ -195,17 +191,16 @@ BOOST_AUTO_TEST_CASE(test_push_adapter) req.push("SUBSCRIBE", "channel"); req.push("PING"); - conn.async_receive(error_tag_obj, [](auto ec, auto) { + conn->async_receive(error_tag_obj, [conn](auto ec, auto) { BOOST_CHECK_EQUAL(ec, boost::redis::error::incompatible_size); + conn->cancel(operation::reconnection); }); - conn.async_exec(req, ignore, [](auto ec, auto){ + conn->async_exec(req, ignore, [](auto ec, auto){ BOOST_CHECK_EQUAL(ec, net::experimental::error::channel_errors::channel_cancelled); }); - async_run(conn, address{}, 10s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); - }); + run(conn); ioc.run(); @@ -213,10 +208,10 @@ BOOST_AUTO_TEST_CASE(test_push_adapter) // reconnection is possible after an error. } -net::awaitable push_consumer3(connection& conn) +net::awaitable push_consumer3(std::shared_ptr conn) { for (;;) { - co_await conn.async_receive(ignore, net::use_awaitable); + co_await conn->async_receive(ignore, net::use_awaitable); } } @@ -239,75 +234,73 @@ BOOST_AUTO_TEST_CASE(many_subscribers) req3.push("QUIT"); net::io_context ioc; - connection conn{ioc}; + auto conn = std::make_shared(ioc); auto c11 =[&](auto ec, auto...) { std::cout << "quit sent" << std::endl; + conn->cancel(operation::reconnection); BOOST_TEST(!ec); }; auto c10 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req3, ignore, c11); + conn->async_exec(req3, ignore, c11); }; auto c9 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c10); + conn->async_exec(req2, ignore, c10); }; auto c8 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c9); + conn->async_exec(req1, ignore, c9); }; auto c7 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c8); + conn->async_exec(req2, ignore, c8); }; auto c6 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c7); + conn->async_exec(req2, ignore, c7); }; auto c5 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c6); + conn->async_exec(req1, ignore, c6); }; auto c4 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c5); + conn->async_exec(req2, ignore, c5); }; auto c3 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c4); + conn->async_exec(req1, ignore, c4); }; auto c2 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c3); + conn->async_exec(req2, ignore, c3); }; auto c1 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); + conn->async_exec(req2, ignore, c2); }; auto c0 =[&](auto ec, auto...) { BOOST_TEST(!ec); - conn.async_exec(req1, ignore, c1); + conn->async_exec(req1, ignore, c1); }; - conn.async_exec(req0, ignore, c0); + conn->async_exec(req0, ignore, c0); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - BOOST_TEST(!ec); - conn.cancel(operation::receive); - }); + run(conn, {}, {}); net::co_spawn(ioc.get_executor(), push_consumer3(conn), net::detached); ioc.run(); diff --git a/tests/conn_quit.cpp b/tests/conn_quit.cpp index 77661544..0d5ba9a2 100644 --- a/tests/conn_quit.cpp +++ b/tests/conn_quit.cpp @@ -4,55 +4,53 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include #include #define BOOST_TEST_MODULE conn-quit #include -#include "common.hpp" #include + +// TODO: Move this to a lib. #include namespace net = boost::asio; - -using connection = boost::redis::connection; -using error_code = boost::system::error_code; -using operation = boost::redis::operation; +using boost::redis::connection; +using boost::system::error_code; +using boost::redis::operation; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; -using boost::redis::async_run; -using boost::redis::logger; -using boost::redis::address; +using boost::redis::config; using namespace std::chrono_literals; -BOOST_AUTO_TEST_CASE(test_quit1) +BOOST_AUTO_TEST_CASE(test_eof_no_error) { request req; req.get_config().cancel_on_connection_lost = false; - req.push("HELLO", 3); req.push("QUIT"); net::io_context ioc; connection conn{ioc}; - conn.async_exec(req, ignore, [](auto ec, auto) { + conn.async_exec(req, ignore, [&](auto ec, auto) { BOOST_TEST(!ec); + conn.cancel(operation::reconnection); }); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - BOOST_TEST(!ec); + conn.async_run({}, {}, [](auto ec){ + BOOST_TEST(!!ec); }); ioc.run(); } // Test if quit causes async_run to exit. -BOOST_AUTO_TEST_CASE(test_quit2) +BOOST_AUTO_TEST_CASE(test_async_run_exits) { net::io_context ioc; connection conn{ioc}; + conn.cancel(operation::reconnection); request req1; req1.get_config().cancel_on_connection_lost = false; @@ -62,37 +60,39 @@ BOOST_AUTO_TEST_CASE(test_quit2) req2.get_config().cancel_on_connection_lost = false; req2.push("QUIT"); + // Should fail since this request will be sent after quit. request req3; - // Should cause the request to fail since this request will be sent - // after quit. req3.get_config().cancel_if_not_connected = true; req3.push("PING"); auto c3 = [](auto ec, auto) { - std::cout << "3--> " << ec.message() << std::endl; + std::clog << "c3: " << ec.message() << std::endl; BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled); }; auto c2 = [&](auto ec, auto) { - std::cout << "2--> " << ec.message() << std::endl; + std::clog << "c2: " << ec.message() << std::endl; BOOST_TEST(!ec); conn.async_exec(req3, ignore, c3); }; auto c1 = [&](auto ec, auto) { - std::cout << "1--> " << ec.message() << std::endl; + std::cout << "c3: " << ec.message() << std::endl; BOOST_TEST(!ec); - conn.async_exec(req2, ignore, c2); }; conn.async_exec(req1, ignore, c1); - async_run(conn, address{}, 10s, 10s, logger{}, [&](auto ec){ - BOOST_TEST(!ec); + // The healthy checker should not be the cause of async_run + // completing, so we set a long timeout. + config cfg; + cfg.health_check_interval = 10000s; + conn.async_run({}, {}, [&](auto ec){ + BOOST_TEST(!!ec); }); ioc.run(); diff --git a/tests/conn_reconnect.cpp b/tests/conn_reconnect.cpp index fab680ad..ce948709 100644 --- a/tests/conn_reconnect.cpp +++ b/tests/conn_reconnect.cpp @@ -4,8 +4,7 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include #include #define BOOST_TEST_MODULE conn-reconnect #include @@ -18,14 +17,14 @@ #include namespace net = boost::asio; -using error_code = boost::system::error_code; +using boost::system::error_code; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; -using boost::redis::async_run; +using boost::redis::config; using boost::redis::logger; -using boost::redis::address; -using connection = boost::asio::use_awaitable_t<>::as_default_on_t; +using boost::redis::operation; +using boost::redis::connection; using namespace std::chrono_literals; using namespace boost::asio::experimental::awaitable_operators; @@ -37,22 +36,20 @@ net::awaitable test_reconnect_impl() request req; req.push("QUIT"); - connection conn{ex}; + auto conn = std::make_shared(ex); + run(conn); int i = 0; - address addr; for (; i < 5; ++i) { - boost::system::error_code ec1, ec2; - co_await ( - conn.async_exec(req, ignore, net::redirect_error(net::use_awaitable, ec1)) && - async_run(conn, addr, 10s, 10s, logger{}, net::redirect_error(net::use_awaitable, ec2)) - ); - - BOOST_TEST(!ec1); - BOOST_TEST(!ec2); - conn.reset_stream(); + error_code ec1, ec2; + config cfg; + logger l; + co_await conn->async_exec(req, ignore, net::redirect_error(net::use_awaitable, ec1)); + //BOOST_TEST(!ec); + std::cout << "test_reconnect: " << i << " " << ec2.message() << " " << ec1.message() << std::endl; } + conn->cancel(); BOOST_CHECK_EQUAL(i, 5); co_return; } @@ -72,44 +69,40 @@ auto async_test_reconnect_timeout() -> net::awaitable net::steady_timer st{ex}; auto conn = std::make_shared(ex); - boost::system::error_code ec1, ec2, ec3; + error_code ec1, ec3; request req1; req1.get_config().cancel_if_not_connected = false; req1.get_config().cancel_on_connection_lost = true; req1.get_config().cancel_if_unresponded = true; - req1.push("HELLO", 3); req1.push("BLPOP", "any", 0); st.expires_after(std::chrono::seconds{1}); - address addr; + config cfg; co_await ( conn->async_exec(req1, ignore, redir(ec1)) || - async_run(*conn, addr, 10s, 10s, logger{}, redir(ec2)) || st.async_wait(redir(ec3)) ); //BOOST_TEST(!ec1); - BOOST_CHECK_EQUAL(ec2, boost::system::errc::errc_t::operation_canceled); //BOOST_TEST(!ec3); request req2; req2.get_config().cancel_if_not_connected = false; req2.get_config().cancel_on_connection_lost = true; req2.get_config().cancel_if_unresponded= true; - req2.push("HELLO", 3); req2.push("QUIT"); st.expires_after(std::chrono::seconds{1}); co_await ( conn->async_exec(req1, ignore, net::redirect_error(net::use_awaitable, ec1)) || - async_run(*conn, addr, 10s, 10s, logger{}, net::redirect_error(net::use_awaitable, ec2)) || st.async_wait(net::redirect_error(net::use_awaitable, ec3)) ); + conn->cancel(); + std::cout << "ccc" << std::endl; BOOST_CHECK_EQUAL(ec1, boost::system::errc::errc_t::operation_canceled); - BOOST_CHECK_EQUAL(ec2, boost::asio::error::basic_errors::operation_aborted); } BOOST_AUTO_TEST_CASE(test_reconnect_and_idle) diff --git a/tests/conn_run_cancel.cpp b/tests/conn_run_cancel.cpp index 3f0a8abd..d9f383f1 100644 --- a/tests/conn_run_cancel.cpp +++ b/tests/conn_run_cancel.cpp @@ -4,7 +4,7 @@ * accompanying file LICENSE.txt) */ -#include +#include #include #include #include @@ -21,15 +21,14 @@ namespace net = boost::asio; using boost::redis::operation; -using connection = boost::redis::connection; -using error_code = boost::system::error_code; +using boost::redis::config; +using boost::redis::connection; +using boost::system::error_code; using net::experimental::as_tuple; using boost::redis::request; using boost::redis::response; using boost::redis::ignore; -using boost::redis::async_run; using boost::redis::logger; -using boost::redis::address; using namespace std::chrono_literals; using namespace net::experimental::awaitable_operators; @@ -40,13 +39,14 @@ auto async_cancel_run_with_timer() -> net::awaitable connection conn{ex}; net::steady_timer st{ex}; - st.expires_after(std::chrono::seconds{1}); + st.expires_after(1s); - boost::system::error_code ec1, ec2; - address addr; - co_await (async_run(conn, addr, 10s, 10s, logger{}, redir(ec1)) || st.async_wait(redir(ec2))); + error_code ec1, ec2; + config cfg; + logger l; + co_await (conn.async_run(cfg, l, redir(ec1)) || st.async_wait(redir(ec2))); - BOOST_CHECK_EQUAL(ec1, boost::asio::error::basic_errors::operation_aborted); + BOOST_CHECK_EQUAL(ec1, boost::asio::error::operation_aborted); BOOST_TEST(!ec2); } @@ -67,10 +67,11 @@ async_check_cancellation_not_missed(int n, std::chrono::milliseconds ms) -> net: for (auto i = 0; i < n; ++i) { timer.expires_after(ms); - boost::system::error_code ec1, ec2; - address addr; - co_await (async_run(conn, addr, 10s, 10s, logger{}, redir(ec1)) || timer.async_wait(redir(ec2))); - BOOST_CHECK_EQUAL(ec1, boost::asio::error::basic_errors::operation_aborted); + error_code ec1, ec2; + config cfg; + logger l; + co_await (conn.async_run(cfg, l, redir(ec1)) || timer.async_wait(redir(ec2))); + BOOST_CHECK_EQUAL(ec1, boost::asio::error::operation_aborted); std::cout << "Counter: " << i << std::endl; } } @@ -146,28 +147,6 @@ BOOST_AUTO_TEST_CASE(check_implicit_cancel_not_missed_1024) ioc.run(); } -BOOST_AUTO_TEST_CASE(reset_before_run_completes) -{ - net::io_context ioc; - connection conn{ioc}; - - // Sends a ping just as a means of waiting until we are connected. - request req; - req.push("HELLO", 3); - req.push("PING"); - - conn.async_exec(req, ignore, [&](auto ec, auto){ - BOOST_TEST(!ec); - conn.reset_stream(); - }); - address addr; - async_run(conn, addr, 10s, 10s, logger{}, [&](auto ec){ - BOOST_CHECK_EQUAL(ec, net::error::operation_aborted); - }); - - ioc.run(); -} - #else BOOST_AUTO_TEST_CASE(dummy) { diff --git a/tests/conn_tls.cpp b/tests/conn_tls.cpp index 38fcbfde..7a62b3b1 100644 --- a/tests/conn_tls.cpp +++ b/tests/conn_tls.cpp @@ -4,13 +4,12 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include #define BOOST_TEST_MODULE conn-tls #include -#include #include #include "common.hpp" + #include namespace net = boost::asio; @@ -18,24 +17,8 @@ namespace net = boost::asio; using connection = boost::redis::ssl::connection; using boost::redis::request; using boost::redis::response; -using boost::redis::ignore_t; - -using endpoints = net::ip::tcp::resolver::results_type; - -auto -resolve( - std::string const& host = "127.0.0.1", - std::string const& port = "6379") -> endpoints -{ - net::io_context ioc; - net::ip::tcp::resolver resv{ioc}; - return resv.resolve(host, port); -} - -struct endpoint { - std::string host; - std::string port; -}; +using boost::redis::config; +using boost::redis::operation; bool verify_certificate(bool, net::ssl::verify_context&) { @@ -45,17 +28,18 @@ bool verify_certificate(bool, net::ssl::verify_context&) BOOST_AUTO_TEST_CASE(ping) { + config cfg; + cfg.username = "aedis"; + cfg.password = "aedis"; + cfg.addr.host = "db.occase.de"; + cfg.addr.port = "6380"; + std::string const in = "Kabuf"; request req; - req.get_config().cancel_on_connection_lost = true; - req.push("HELLO", 3, "AUTH", "aedis", "aedis"); req.push("PING", in); - req.push("QUIT"); - response resp; - - auto const endpoints = resolve("db.occase.de", "6380"); + response resp; net::io_context ioc; net::ssl::context ctx{net::ssl::context::sslv23}; @@ -63,19 +47,16 @@ BOOST_AUTO_TEST_CASE(ping) conn.next_layer().set_verify_mode(net::ssl::verify_peer); conn.next_layer().set_verify_callback(verify_certificate); - net::connect(conn.lowest_layer(), endpoints); - conn.next_layer().handshake(net::ssl::stream_base::client); - - conn.async_exec(req, resp, [](auto ec, auto) { + conn.async_exec(req, resp, [&](auto ec, auto) { BOOST_TEST(!ec); + conn.cancel(); }); - conn.async_run([](auto ec) { - BOOST_TEST(!ec); - }); + conn.async_run(cfg, {}, [](auto) { }); ioc.run(); - BOOST_CHECK_EQUAL(in, std::get<1>(resp).value()); + BOOST_CHECK_EQUAL(in, std::get<0>(resp).value()); + std::cout << "===============================" << std::endl; } diff --git a/tests/cpp20_low_level_async.cpp b/tests/cpp20_low_level_async.cpp index eba51e5a..cb6776b7 100644 --- a/tests/cpp20_low_level_async.cpp +++ b/tests/cpp20_low_level_async.cpp @@ -4,8 +4,8 @@ * accompanying file LICENSE.txt) */ +#include #include -#include #include #include #include @@ -22,15 +22,15 @@ using tcp_socket = net::use_awaitable_t<>::as_default_on_t using boost::redis::adapter::adapt2; using net::ip::tcp; using boost::redis::request; -using boost::redis::address; using boost::redis::adapter::result; +using redis::config; -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; resolver resv{ex}; - auto const addrs = co_await resv.async_resolve(addr.host, addr.port); + auto const addrs = co_await resv.async_resolve(cfg.addr.host, cfg.addr.port); tcp_socket socket{ex}; co_await net::async_connect(socket, addrs); diff --git a/tests/issue_50.cpp b/tests/issue_50.cpp index c5394333..7b27df6b 100644 --- a/tests/issue_50.cpp +++ b/tests/issue_50.cpp @@ -6,7 +6,7 @@ // Must come before any asio header, otherwise build fails on msvc. -#include +#include #include #include #include @@ -22,31 +22,44 @@ #if defined(BOOST_ASIO_HAS_CO_AWAIT) namespace net = boost::asio; -namespace redis = boost::redis; using steady_timer = net::use_awaitable_t<>::as_default_on_t; -using redis::request; -using redis::response; -using redis::ignore; -using redis::address; -using redis::logger; -using redis::experimental::async_connect; -using redis::experimental::connect_config; +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::system::error_code; +using boost::asio::use_awaitable; +using boost::asio::redirect_error; using connection = boost::asio::use_awaitable_t<>::as_default_on_t; using namespace std::chrono_literals; // Push consumer -auto receiver(std::shared_ptr conn) -> net::awaitable +auto +receiver(std::shared_ptr conn) -> net::awaitable { - boost::system::error_code ec; - while (!ec) - co_await conn->async_receive(ignore, net::redirect_error(net::use_awaitable, ec)); + std::cout << "uuu" << std::endl; + while (!conn->is_cancelled()) { + std::cout << "dddd" << std::endl; + // Loop reading Redis pushs messages. + for (;;) { + std::cout << "aaaa" << std::endl; + error_code ec; + co_await conn->async_receive(ignore, redirect_error(use_awaitable, ec)); + if (ec) + break; + } + } } -auto periodic_task(std::shared_ptr conn) -> net::awaitable +auto +periodic_task(std::shared_ptr conn) -> net::awaitable { net::steady_timer timer{co_await net::this_coro::executor}; for (int i = 0; i < 10; ++i) { - timer.expires_after(std::chrono::seconds(2)); + std::cout << "In the loop: " << i << std::endl; + timer.expires_after(std::chrono::milliseconds(50)); co_await timer.async_wait(net::use_awaitable); // Key is not set so it will cause an error since we are passing @@ -56,29 +69,26 @@ auto periodic_task(std::shared_ptr conn) -> net::awaitable req.push("GET", "mykey"); auto [ec, u] = co_await conn->async_exec(req, ignore, net::as_tuple(net::use_awaitable)); if (ec) { - std::cout << "Error: " << ec << std::endl; + std::cout << "(1)Error: " << ec << std::endl; } else { std::cout << "no error: " << std::endl; } } std::cout << "Periodic task done!" << std::endl; - conn->disable_reconnection(); - conn->cancel(redis::operation::run); - conn->cancel(redis::operation::receive); + conn->cancel(operation::run); + conn->cancel(operation::receive); + conn->cancel(operation::reconnection); } -auto co_main(address const& addr) -> net::awaitable +auto co_main(config const& cfg) -> net::awaitable { auto ex = co_await net::this_coro::executor; auto conn = std::make_shared(ex); - connect_config cfg; - cfg.addr = addr; - net::co_spawn(ex, receiver(conn), net::detached); net::co_spawn(ex, periodic_task(conn), net::detached); - redis::experimental::async_connect(*conn, cfg, logger{}, net::consign(net::detached, conn)); + conn->async_run(cfg, {}, net::consign(net::detached, conn)); } #endif // defined(BOOST_ASIO_HAS_CO_AWAIT) diff --git a/tests/run.cpp b/tests/run.cpp index 34c29d8b..3e2740e9 100644 --- a/tests/run.cpp +++ b/tests/run.cpp @@ -4,19 +4,20 @@ * accompanying file LICENSE.txt) */ -#include -#include +#include #define BOOST_TEST_MODULE run #include #include #include +#include "common.hpp" namespace net = boost::asio; +namespace redis = boost::redis; -using connection = boost::redis::connection; -using boost::redis::async_run; -using boost::redis::logger; -using boost::redis::address; +using connection = redis::connection; +using redis::config; +using redis::logger; +using redis::operation; using boost::system::error_code; using namespace std::chrono_literals; @@ -31,8 +32,16 @@ BOOST_AUTO_TEST_CASE(resolve_bad_host) { net::io_context ioc; + config cfg; + cfg.addr.host = "Atibaia"; + cfg.addr.port = "6379"; + cfg.resolve_timeout = 10h; + cfg.connect_timeout = 10h; + cfg.health_check_interval = 10h; + connection conn{ioc}; - async_run(conn, address{{"Atibaia"}, {"6379"}}, 1000s, 1000s, logger{}, [](auto ec){ + conn.cancel(operation::reconnection); + conn.async_run(cfg, {}, [](auto ec){ BOOST_TEST(is_host_not_found(ec)); }); @@ -43,11 +52,16 @@ BOOST_AUTO_TEST_CASE(resolve_with_timeout) { net::io_context ioc; - connection conn{ioc}; - async_run(conn, address{{"Atibaia"}, {"6379"}}, 1ms, 1ms, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::redis::error::resolve_timeout); - }); + config cfg; + cfg.addr.host = "occase.de"; + cfg.addr.port = "6379"; + cfg.resolve_timeout = 1ms; + cfg.connect_timeout = 1ms; + cfg.health_check_interval = 10h; + auto conn = std::make_shared(ioc); + conn->cancel(operation::reconnection); + run(conn, cfg); ioc.run(); } @@ -55,23 +69,33 @@ BOOST_AUTO_TEST_CASE(connect_bad_port) { net::io_context ioc; - connection conn{ioc}; - async_run(conn, address{{"127.0.0.1"}, {"1"}}, 1000s, 10s, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, net::error::basic_errors::connection_refused); - }); + config cfg; + cfg.addr.host = "127.0.0.1"; + cfg.addr.port = "1"; + cfg.resolve_timeout = 10h; + cfg.connect_timeout = 10s; + cfg.health_check_interval = 10h; + auto conn = std::make_shared(ioc); + conn->cancel(operation::reconnection); + run(conn, cfg, net::error::connection_refused); ioc.run(); } -BOOST_AUTO_TEST_CASE(connect_with_timeout) -{ - net::io_context ioc; - - connection conn{ioc}; - async_run(conn, address{{"example.com"}, {"1"}}, 10s, 1ms, logger{}, [](auto ec){ - BOOST_CHECK_EQUAL(ec, boost::redis::error::connect_timeout); - }); - - ioc.run(); -} +// Hard to test. +//BOOST_AUTO_TEST_CASE(connect_with_timeout) +//{ +// net::io_context ioc; +// +// config cfg; +// cfg.addr.host = "example.com"; +// cfg.addr.port = "80"; +// cfg.resolve_timeout = 10s; +// cfg.connect_timeout = 1ns; +// cfg.health_check_interval = 10h; +// +// auto conn = std::make_shared(ioc); +// run(conn, cfg, boost::redis::error::connect_timeout); +// ioc.run(); +//}